This commit is contained in:
t. boddy 2026-03-18 13:23:51 -04:00
parent 417cae168f
commit a8bc01bedd
59 changed files with 2053 additions and 1054 deletions

View file

@ -2,5 +2,6 @@ rm -rf res/resources.o res/resources.h out/*
# make # make
# ./blastem/blastem out.bin # ./blastem/blastem out.bin
#dgen out.bin #dgen out.bin
docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00 docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.11
/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin # /Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin
/Applications/ares.app/Contents/MacOS/ares out/rom.bin --system "Mega Drive"

View file

@ -1,2 +1,2 @@
rm -rf res/resources.o res/resources.h out/* rm -rf res/resources.o res/resources.h out/*
docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00 docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.11

BIN
default.profraw Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

BIN
res/door.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
res/enemies/boss1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
res/enemies/boss2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
res/enemies/boss3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
res/enemies/boss4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
res/eyebig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

BIN
res/fontbigger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

BIN
res/musicroom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,19 +1,22 @@
IMAGE font "font.png" NONE NONE IMAGE font "font.png" NONE NONE
IMAGE shadow "shadow.png" NONE NONE IMAGE shadow "shadow.png" NONE NONE
IMAGE startFade1 "start/fade1.png" FAST
IMAGE startFade2 "start/fade2.png" FAST
IMAGE startFade3 "start/fade3.png" FAST
IMAGE startFade4 "start/fade4.png" FAST
IMAGE startSplash1 "start/splash1.png" FAST IMAGE startSplash1 "start/splash1.png" FAST
IMAGE startLogo "start/logo.png" FAST IMAGE startLogo "start/logo.png" FAST
IMAGE startBigBg "start/bigbg.png" FAST IMAGE musicroom "musicroom.png" FAST
IMAGE startBg1 "start/bg1.png" FAST
IMAGE startBg2 "start/bg2.png" FAST
IMAGE startBg3 "start/bg3.png" FAST
IMAGE startBg4 "start/bg4.png" FAST
IMAGE startBg5 "start/bg5.png" FAST
IMAGE startBg6 "start/bg6.png" FAST
XGM2 bgmStart "start.vgm" XGM2 bgmStart "start.vgm"
IMAGE sky "sky.png" NONE NONE IMAGE sky "sky.png" NONE NONE
IMAGE skyTop "skytop.png" NONE NONE IMAGE skyTop "skytop.png" NONE NONE
IMAGE skyRed "skyred.png" NONE NONE IMAGE skyRed "skyred.png" NONE NONE
IMAGE ground "ground.png" NONE NONE IMAGE ground "ground.png" NONE NONE
IMAGE door "door.png" NONE NONE
SPRITE momoyoSprite "momoyo.png" 6 6 NONE 0 SPRITE momoyoSprite "momoyo.png" 6 6 NONE 0
@ -21,13 +24,39 @@ SPRITE bulletsSprite "bullets.png" 2 2 NONE 0
SPRITE pBulletSprite "pbullet.png" 4 4 NONE 0 SPRITE pBulletSprite "pbullet.png" 4 4 NONE 0
SPRITE fairySprite "fairy2.png" 4 4 NONE 8 SPRITE fairySprite "fairy2.png" 4 4 NONE 8
SPRITE eyeBigSprite "eyebig.png" 4 4 NONE 0
SPRITE boss1Sprite "enemies/boss1.png" 6 6 NONE 0
SPRITE boss2Sprite "enemies/boss2.png" 6 6 NONE 0
SPRITE boss3Sprite "enemies/boss3.png" 6 6 NONE 0
SPRITE boss4Sprite "enemies/boss4.png" 6 6 NONE 0
SPRITE treasureSprite "treasure.png" 4 4 NONE 0 SPRITE treasureSprite "treasure.png" 4 4 NONE 0
IMAGE mapIndicator "mapindicator.png" NONE NONE IMAGE mapIndicator "mapindicator.png" NONE NONE
TILESET starTiles "stars.png" NONE
IMAGE imageFontBig "fontbig.png" NONE NONE IMAGE imageFontBig "fontbig.png" NONE NONE
IMAGE imageFontBigger "fontbigger.png" NONE NONE
IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE
IMAGE imageChromeLife "life.png" NONE NONE IMAGE imageChromeLife "life.png" NONE NONE
IMAGE imageChromeLife2 "life2.png" NONE NONE IMAGE imageChromeLife2 "life2.png" NONE NONE
XGM2 stageMusic "level.vgm" XGM2 stageMusic "level.vgm"
XGM2 bossMusic "boss.vgm"
XGM2 treasureMusic "treasure.vgm"
WAV sfxSamplePlayerShot "sfx/playershot.wav" XGM2
WAV sfxSampleBullet1 "sfx/bullet1.wav" XGM2
WAV sfxSampleBullet2 "sfx/bullet2.wav" XGM2
WAV sfxSampleBullet3 "sfx/bullet3.wav" XGM2
WAV sfxSampleExplosion "sfx/explosion1.wav" XGM2
WAV sfxSamplePickup "sfx/menuchoose.wav" XGM2
WAV sfxSampleGraze "sfx/menuselect.wav" XGM2
WAV sfxSampleStartGame "sfx/menuchoose.wav" XGM2
WAV sfxSamplePlayerHit "sfx/playerhit.wav" XGM2
WAV sfxSampleMenuChoose "sfx/menuchoose.wav" XGM2
WAV sfxSampleMenuSelect "sfx/menuselect.wav" XGM2
WAV sfxSampleGameOver "sfx/gameover.wav" XGM2
WAV sfxSampleCollectTreasure "sfx/beatlevel.wav" XGM2
WAV sfxSampleCollectAllTreasures "sfx/startgame.wav" XGM2

BIN
res/sfx/beatgame.wav Normal file

Binary file not shown.

BIN
res/sfx/beatlevel.wav Normal file

Binary file not shown.

BIN
res/sfx/bullet1.wav Normal file

Binary file not shown.

BIN
res/sfx/bullet2.wav Normal file

Binary file not shown.

BIN
res/sfx/bullet3.wav Normal file

Binary file not shown.

BIN
res/sfx/explosion1.wav Normal file

Binary file not shown.

BIN
res/sfx/explosion2.wav Normal file

Binary file not shown.

BIN
res/sfx/gameover.wav Normal file

Binary file not shown.

BIN
res/sfx/menuchoose.wav Normal file

Binary file not shown.

BIN
res/sfx/menuselect.wav Normal file

Binary file not shown.

BIN
res/sfx/playerhit.wav Normal file

Binary file not shown.

BIN
res/sfx/playershot.wav Normal file

Binary file not shown.

BIN
res/sfx/startgame.wav Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

BIN
res/stars.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
res/start/bg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
res/start/bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
res/start/bg3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
res/start/bg4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
res/start/bg5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
res/start/bg6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Before After
Before After

3
run.sh
View file

@ -1 +1,2 @@
/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin #/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin
/Applications/ares.app/Contents/MacOS/ares out/rom.bin --system "Mega Drive"

View file

@ -1,11 +1,13 @@
#define BG_I 8 #define BG_I 8
// zone-unique block: 64x64px ground block in sky area, only visible in zone 0 // doors: one per zone, placed in sky area at tile row 16
// world X=256 = tile col 32, placed in sky row block 1 (tile row 8) // base X per zone chosen so tile cols never overlap between zone pairs:
#define ZONE_BLOCK_WORLD_X 256 // zone 0 → cols 1-31, zone 2 → cols 33-63
#define ZONE_BLOCK_COL ((ZONE_BLOCK_WORLD_X / 8) % 128) // zone 1 → cols 65-95, zone 3 → cols 97-127
#define ZONE_BLOCK_ROW 16 #define ZONE_BLOCK_ROW 16
bool zoneBlockVisible; #define DOOR_COUNT SECTION_COUNT
fix32 doorWorldX[DOOR_COUNT];
bool doorVisible[DOOR_COUNT];
fix32 prevCamera; fix32 prevCamera;
#define PARALLAX_COUNT 8 #define PARALLAX_COUNT 8
fix32 parallaxAccum[PARALLAX_COUNT]; fix32 parallaxAccum[PARALLAX_COUNT];
@ -15,6 +17,7 @@ static const fix32 parallaxMul[PARALLAX_COUNT] = {
s16 bgScroll[28]; s16 bgScroll[28];
u8 bgOff; u8 bgOff;
u16 bgPal[16];
void loadBackground(){ void loadBackground(){
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE); VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
@ -23,6 +26,7 @@ void loadBackground(){
VDP_loadTileSet(sky.tileset, BG_I + 64, DMA); VDP_loadTileSet(sky.tileset, BG_I + 64, DMA);
VDP_loadTileSet(ground.tileset, BG_I + 128, DMA); VDP_loadTileSet(ground.tileset, BG_I + 128, DMA);
VDP_loadTileSet(skyRed.tileset, BG_I + 192, DMA); VDP_loadTileSet(skyRed.tileset, BG_I + 192, DMA);
VDP_loadTileSet(door.tileset, BG_I + 256, DMA);
// for(u8 y = 0; y < 14; y++){ // for(u8 y = 0; y < 14; y++){
// for(u8 x = 0; x < 64; x++){ // for(u8 x = 0; x < 64; x++){
@ -41,29 +45,35 @@ void loadBackground(){
// } // }
// } // }
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 192), 0, 0, 128, 8); VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 192), 0, 0, 128, 8);
for(u8 y = 0; y < 3; y++){ for(u8 y = 0; y < 3; y++){
for(u8 x = 0; x < 16; x++){ for(u8 x = 0; x < 16; x++){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 * y), x * 8, y * 8 + 8, 8, 8); VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 64 * y), x * 8, y * 8 + 8, 8, 8);
} }
} }
// place 64x64 ground block in sky area (zone 0 only) // place one door per zone at a random position within each zone's unique col range
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); for(u8 d = 0; d < DOOR_COUNT; d++){
zoneBlockVisible = TRUE; doorWorldX[d] = FIX32(d * 512 + 8 + (random() % 31) * 8);
u8 col = (u8)(F32_toInt(doorWorldX[d]) / 8 % 128);
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 256), col, ZONE_BLOCK_ROW, 8, 8);
doorVisible[d] = TRUE;
}
prevCamera = player.camera; prevCamera = player.camera;
for(u8 i = 0; i < PARALLAX_COUNT; i++) for(u8 i = 0; i < PARALLAX_COUNT; i++)
parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]); parallaxAccum[i] = F32_mul(player.camera + FIX32(256), parallaxMul[i]);
// write initial scroll values so first frame has correct parallax // write initial scroll values so first frame has correct parallax
s16 initScroll = fix32ToInt(-player.camera); s16 initScroll = F32_toInt(-player.camera);
for(u8 i = 0; i < 20; i++)
bgScroll[i] = initScroll;
for(u8 i = 0; i < 8; i++) for(u8 i = 0; i < 8; i++)
bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i])); bgScroll[27 - i] = (initScroll - F32_toInt(parallaxAccum[i]));
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
} }
void updateBackground(){ void updateBackground(){
s16 scrollVal = fix32ToInt(-player.camera); s16 scrollVal = F32_toInt(-player.camera);
// accumulate parallax from camera delta (not absolute position) // accumulate parallax from camera delta (not absolute position)
// this avoids discontinuities at world wrap boundaries // this avoids discontinuities at world wrap boundaries
@ -75,7 +85,7 @@ void updateBackground(){
// update accumulators once, reuse for top and bottom // update accumulators once, reuse for top and bottom
for(u8 i = 0; i < PARALLAX_COUNT; i++){ for(u8 i = 0; i < PARALLAX_COUNT; i++){
parallaxAccum[i] += fix32Mul(delta, parallaxMul[i]); parallaxAccum[i] += F32_mul(delta, parallaxMul[i]);
if(parallaxAccum[i] > FIX32(1024)) parallaxAccum[i] -= FIX32(1024); if(parallaxAccum[i] > FIX32(1024)) parallaxAccum[i] -= FIX32(1024);
else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024); else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024);
} }
@ -83,18 +93,51 @@ void updateBackground(){
for(u8 i = 0; i < 20; i++) for(u8 i = 0; i < 20; i++)
bgScroll[i] = scrollVal; bgScroll[i] = scrollVal;
for(u8 i = 0; i < 8; i++) for(u8 i = 0; i < 8; i++)
bgScroll[27 - i] = (scrollVal - fix32ToInt(parallaxAccum[i])); bgScroll[27 - i] = (scrollVal - F32_toInt(parallaxAccum[i]));
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
// show ground block only when zone 0 copy of these columns is on screen // show/hide each door based on proximity to camera center
fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160)); for(u8 d = 0; d < DOOR_COUNT; d++){
bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212)); fix32 dx = getWrappedDelta(doorWorldX[d] + FIX32(32), player.camera + FIX32(160));
// if(shouldShow && !zoneBlockVisible){ bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212));
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); u8 col = (u8)(F32_toInt(doorWorldX[d]) / 8 % 128);
// zoneBlockVisible = TRUE; if(shouldShow && !doorVisible[d]){
// } else if(!shouldShow && zoneBlockVisible){ VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 256), col, ZONE_BLOCK_ROW, 8, 8);
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); doorVisible[d] = TRUE;
// zoneBlockVisible = FALSE; } else if(!shouldShow && doorVisible[d]){
// } VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 64), col, ZONE_BLOCK_ROW, 8, 8);
doorVisible[d] = FALSE;
}
}
}
#define BG_THEME_RED 0
#define BG_THEME_GREEN 1
#define BG_THEME_BLUE 2
void loadBgPalette(u8 theme) {
u16 coloredPalette[16];
u8 i;
for(i = 0; i < 16; i++) {
u16 color = shadow.palette->data[i];
u16 r = color & 0xF;
u16 g = (color >> 4) & 0xF;
u16 b = (color >> 8) & 0xF;
switch(theme) {
case BG_THEME_GREEN:
coloredPalette[i] = (b << 8) | (r << 4) | g;
break;
case BG_THEME_BLUE: {
u16 newB = r > b ? r : b;
coloredPalette[i] = (newB << 8) | (g << 4) | (r >> 1);
break;
}
default: // BG_THEME_RED
coloredPalette[i] = color;
break;
}
}
memcpy(bgPal, coloredPalette, 16 * sizeof(u16));
PAL_setPalette(PAL2, coloredPalette, DMA_QUEUE);
} }

View file

@ -92,126 +92,6 @@ SkipInit:
* *
*------------------------------------------------ *------------------------------------------------
registersDump:
move.l %d0,registerState+0
move.l %d1,registerState+4
move.l %d2,registerState+8
move.l %d3,registerState+12
move.l %d4,registerState+16
move.l %d5,registerState+20
move.l %d6,registerState+24
move.l %d7,registerState+28
move.l %a0,registerState+32
move.l %a1,registerState+36
move.l %a2,registerState+40
move.l %a3,registerState+44
move.l %a4,registerState+48
move.l %a5,registerState+52
move.l %a6,registerState+56
move.l %a7,registerState+60
rts
busAddressErrorDump:
move.w 4(%sp),ext1State
move.l 6(%sp),addrState
move.w 10(%sp),ext2State
move.w 12(%sp),srState
move.l 14(%sp),pcState
jmp registersDump
exception4WDump:
move.w 4(%sp),srState
move.l 6(%sp),pcState
move.w 10(%sp),ext1State
jmp registersDump
exceptionDump:
move.w 4(%sp),srState
move.l 6(%sp),pcState
jmp registersDump
_Bus_Error:
jsr busAddressErrorDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l busErrorCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Address_Error:
jsr busAddressErrorDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l addressErrorCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Illegal_Instruction:
jsr exception4WDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l illegalInstCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Zero_Divide:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l zeroDivideCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Chk_Instruction:
jsr exception4WDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l chkInstCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Trapv_Instruction:
jsr exception4WDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l trapvInstCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Privilege_Violation:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l privilegeViolationCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Trace:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l traceCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Line_1010_Emulation:
_Line_1111_Emulation:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l line1x1xCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Error_Exception:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l errorExceptionCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_INT: _INT:
movem.l %d0-%d1/%a0-%a1,-(%sp) movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l intCB, %a0 move.l intCB, %a0

View file

@ -1,68 +1,39 @@
#define BULLET_OFF 8 #define BULLET_OFF 8
#define P_BULLET_OFF 16 #define P_BULLET_OFF 16
static void doBulletRotation(u8 i){ void doBulletRotation(u8 i){
if(bullets[i].anim >= FIRST_ROTATING_BULLET && !bullets[i].player){ if(bullets[i].anim >= FIRST_ROTATING_BULLET){
bullets[i].vFlip = FALSE; bullets[i].vFlip = FALSE;
bullets[i].hFlip = FALSE; bullets[i].hFlip = FALSE;
if(bullets[i].angle < 0) bullets[i].angle += 1024; fix16 a = F16_normalizeAngle(bullets[i].angle);
else if(bullets[i].angle >= 1024) bullets[i].angle -= 1024; s16 deg = F16_toInt(a);
u8 quadrant = deg / 90;
u8 inQuad = deg % 90;
u8 frame = (inQuad * 9) / 90;
switch(quadrant){
case 0: break;
case 1: frame = 8 - frame; bullets[i].hFlip = TRUE; break;
case 2: bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; break;
case 3: frame = 8 - frame; bullets[i].vFlip = TRUE; break;
}
// 0 - 256 bullets[i].frame = frame;
if(bullets[i].angle >= 1008 || bullets[i].angle < 16) bullets[i].frame = 0;
else if(bullets[i].angle >= 16 && bullets[i].angle < 48) bullets[i].frame = 1;
else if(bullets[i].angle >= 48 && bullets[i].angle < 80) bullets[i].frame = 2;
else if(bullets[i].angle >= 80 && bullets[i].angle < 112) bullets[i].frame = 3;
else if(bullets[i].angle >= 112 && bullets[i].angle < 144) bullets[i].frame = 4;
else if(bullets[i].angle >= 112 && bullets[i].angle < 176) bullets[i].frame = 5;
else if(bullets[i].angle >= 176 && bullets[i].angle < 208) bullets[i].frame = 6;
else if(bullets[i].angle >= 208 && bullets[i].angle < 240) bullets[i].frame = 7;
else if(bullets[i].angle >= 240 && bullets[i].angle < 272) bullets[i].frame = 8;
// 256 - 512
else if(bullets[i].angle >= 272 && bullets[i].angle < 304) { bullets[i].frame = 7; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 304 && bullets[i].angle < 336) { bullets[i].frame = 6; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 336 && bullets[i].angle < 368) { bullets[i].frame = 5; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 368 && bullets[i].angle < 400) { bullets[i].frame = 4; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 400 && bullets[i].angle < 432) { bullets[i].frame = 3; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 432 && bullets[i].angle < 464) { bullets[i].frame = 2; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 464 && bullets[i].angle < 496) { bullets[i].frame = 1; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 496 && bullets[i].angle < 528) { bullets[i].frame = 0; bullets[i].hFlip = TRUE; }
// 512 - 768
else if(bullets[i].angle >= 528 && bullets[i].angle < 560) { bullets[i].frame = 1; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 560 && bullets[i].angle < 592) { bullets[i].frame = 2; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 592 && bullets[i].angle < 624) { bullets[i].frame = 3; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 624 && bullets[i].angle < 656) { bullets[i].frame = 4; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 656 && bullets[i].angle < 688) { bullets[i].frame = 5; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 688 && bullets[i].angle < 720) { bullets[i].frame = 6; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 720 && bullets[i].angle < 752) { bullets[i].frame = 7; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 752 && bullets[i].angle < 784) { bullets[i].frame = 8; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
// 768 - 1024
else if(bullets[i].angle >= 784 && bullets[i].angle < 816) { bullets[i].frame = 7; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 816 && bullets[i].angle < 848) { bullets[i].frame = 6; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 848 && bullets[i].angle < 880) { bullets[i].frame = 5; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 880 && bullets[i].angle < 912) { bullets[i].frame = 4; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 912 && bullets[i].angle < 944) { bullets[i].frame = 3; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 944 && bullets[i].angle < 976) { bullets[i].frame = 2; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 976 && bullets[i].angle < 1008) { bullets[i].frame = 1; bullets[i].vFlip = TRUE; }
SPR_setFrame(bullets[i].image, bullets[i].frame); SPR_setFrame(bullets[i].image, bullets[i].frame);
SPR_setHFlip(bullets[i].image, bullets[i].hFlip); SPR_setHFlip(bullets[i].image, bullets[i].hFlip);
SPR_setVFlip(bullets[i].image, bullets[i].vFlip); SPR_setVFlip(bullets[i].image, bullets[i].vFlip);
} }
} }
void spawnBullet(struct bulletSpawner spawner, void(*updater)){ bool spawnBullet(struct bulletSpawner spawner, void(*updater)){
if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return; if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return FALSE;
// Don't spawn if offscreen // Don't spawn if offscreen -- enemy bullets use tighter visible-screen limit
fix32 dx = getWrappedDelta(spawner.x, player.pos.x); fix32 dx = getWrappedDelta(spawner.x, player.pos.x);
bool offScreenX = (dx < -CULL_LIMIT || dx > CULL_LIMIT); fix32 cullX = spawner.player ? CULL_LIMIT : SCREEN_LIMIT;
bool offScreenX = (dx < -cullX || dx > cullX);
bool offScreenY = (spawner.y < FIX32(-16) || spawner.y > GAME_H_F + FIX32(16)); bool offScreenY = (spawner.y < FIX32(-16) || spawner.y > GAME_H_F + FIX32(16));
if(offScreenX || offScreenY) return; if(offScreenX || offScreenY) return FALSE;
// Find available slot, return if none // Find available slot, return if none
s16 i = -1; s16 i = -1;
@ -74,32 +45,37 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){
break; break;
} }
} }
if(i == -1) return; if(i == -1) return FALSE;
spawner.angle = F16_normalizeAngle(spawner.angle);
bullets[i].active = TRUE; bullets[i].active = TRUE;
bullets[i].pos.x = spawner.x; bullets[i].pos.x = spawner.x;
bullets[i].pos.y = spawner.y; bullets[i].pos.y = spawner.y;
bullets[i].angle = spawner.angle; bullets[i].angle = spawner.angle;
bullets[i].speed = spawner.speed;
bullets[i].player = spawner.player; bullets[i].player = spawner.player;
bullets[i].clock = 0; bullets[i].clock = 0;
if(spawner.vel.x || spawner.vel.y){ if(spawner.vel.x || spawner.vel.y){
bullets[i].vel.x = spawner.vel.x; bullets[i].vel.x = spawner.vel.x;
bullets[i].vel.y = spawner.vel.y; bullets[i].vel.y = spawner.vel.y;
} else { } else {
bullets[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(spawner.angle)), spawner.speed); bullets[i].vel.x = F32_mul(F32_cos(spawner.angle), spawner.speed);
bullets[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(spawner.angle)), spawner.speed); bullets[i].vel.y = F32_mul(F32_sin(spawner.angle), spawner.speed);
} }
bullets[i].updater = updater; bullets[i].updater = updater;
bullets[i].explosion = FALSE; bullets[i].explosion = FALSE;
bullets[i].dist = bullets[i].player ? 16 : (spawner.anim == 0 ? 4 : 7); bullets[i].grazed = FALSE;
bullets[i].dist = bullets[i].player ? 24 : (spawner.anim == 0 ? 4 : 7);
// zero out ints array
for(s16 j = 0; j < PROP_COUNT; j++) bullets[i].ints[j] = spawner.ints[j];
bullets[i].image = SPR_addSprite(spawner.player ? &pBulletSprite : &bulletsSprite, bullets[i].image = SPR_addSprite(&bulletsSprite,
-32, -32, -32, -32,
TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, spawner.player && spawner.angle == 512 ? 1 : 0)); TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, 0));
if(!bullets[i].image){ if(!bullets[i].image){
bullets[i].active = FALSE; bullets[i].active = FALSE;
return; return FALSE;
} }
bullets[i].anim = spawner.anim; bullets[i].anim = spawner.anim;
@ -108,6 +84,12 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){
SPR_setFrame(bullets[i].image, spawner.frame); SPR_setFrame(bullets[i].image, spawner.frame);
SPR_setDepth(bullets[i].image, spawner.player ? 7 : (spawner.top ? 3 : 4)); SPR_setDepth(bullets[i].image, spawner.player ? 7 : (spawner.top ? 3 : 4));
doBulletRotation(i); doBulletRotation(i);
return TRUE;
}
void updateBulletVel(u8 i){
bullets[i].vel.x = F32_mul(F32_cos(bullets[i].angle), bullets[i].speed);
bullets[i].vel.y = F32_mul(F32_sin(bullets[i].angle), bullets[i].speed);
} }
#define BULLET_CHECK FIX32(32) #define BULLET_CHECK FIX32(32)
@ -119,9 +101,9 @@ static void collideWithEnemy(u8 i){
fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y; fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y;
if(deltaY >= -BULLET_CHECK && deltaY <= BULLET_CHECK && if(deltaY >= -BULLET_CHECK && deltaY <= BULLET_CHECK &&
deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){ deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){
bulletDist = getApproximatedDistance(fix32ToInt(deltaX), fix32ToInt(deltaY)); bulletDist = getApproximatedDistance(F32_toInt(deltaX), F32_toInt(deltaY));
if(bulletDist <= bullets[i].dist){ if(bulletDist <= bullets[i].dist){
score += (enemies[j].ints[3] >= 0) ? 200 : 100; score += (enemies[j].ints[3] >= 0) ? 512 : 256;
killBullet(i, TRUE); killBullet(i, TRUE);
killEnemy(j); killEnemy(j);
sfxExplosion(); sfxExplosion();
@ -137,27 +119,54 @@ static void collideWithPlayer(u8 i){
fix32 deltaY = bullets[i].pos.y - player.pos.y; fix32 deltaY = bullets[i].pos.y - player.pos.y;
s32 dist = getApproximatedDistance( s32 dist = getApproximatedDistance(
fix32ToInt(deltaX), F32_toInt(deltaX),
fix32ToInt(deltaY)); F32_toInt(deltaY));
if(dist <= 4){ if(dist <= 4){
// convert enemy bullet to player bullet explosion in-place // kill enemy bullet, then spawn a fresh player bullet explosion
SPR_setDefinition(bullets[i].image, &pBulletSprite); killBullet(i, FALSE);
bullets[i].player = TRUE; s16 expSlot = -1;
bullets[i].pos.x = player.pos.x; for(s16 j = 0; j < BULLET_COUNT; j++) if(!bullets[j].active){ expSlot = j; break; }
bullets[i].pos.y = player.pos.y; if(expSlot >= 0){
killBullet(i, TRUE); bullets[expSlot].active = TRUE;
sfxExplosion(); bullets[expSlot].player = TRUE;
player.lives--; bullets[expSlot].explosion = TRUE;
if(player.lives == 0){ bullets[expSlot].pos.x = player.pos.x;
gameOver = TRUE; bullets[expSlot].pos.y = player.pos.y;
XGM2_stop(); bullets[expSlot].vel.x = 0;
} else { bullets[expSlot].vel.y = 0;
player.respawnClock = 120; bullets[expSlot].clock = 0;
SPR_setVisibility(player.image, HIDDEN); bullets[expSlot].frame = 0;
killBullets = TRUE; bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0));
hitMessageClock = 120; if(bullets[expSlot].image){
hitMessageBullet = TRUE; SPR_setDepth(bullets[expSlot].image, 5);
SPR_setAnim(bullets[expSlot].image, 1);
SPR_setFrame(bullets[expSlot].image, 0);
SPR_setHFlip(bullets[expSlot].image, random() & 1);
} else {
bullets[expSlot].active = FALSE;
}
} }
sfxExplosion();
if(!isAttract){
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
sfxGameOver();
} else {
sfxPlayerHit();
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = TRUE;
}
}
} else if(dist <= GRAZE_RADIUS && !bullets[i].grazed){
bullets[i].grazed = TRUE;
score += 64;
grazeCount++;
sfxGraze();
} }
} }
@ -172,8 +181,8 @@ static void updateBulletExplosion(u8 i){
SPR_setFrame(bullets[i].image, bullets[i].frame); SPR_setFrame(bullets[i].image, bullets[i].frame);
} }
s16 sx = getScreenX(bullets[i].pos.x, player.camera); s16 sx = getScreenX(bullets[i].pos.x, player.camera);
s16 sy = fix32ToInt(bullets[i].pos.y); s16 sy = F32_toInt(bullets[i].pos.y);
u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF; u8 off = BULLET_OFF;
SPR_setPosition(bullets[i].image, sx - off, sy - off); SPR_setPosition(bullets[i].image, sx - off, sy - off);
} }
@ -182,8 +191,8 @@ static void updateBullet(u8 i){
updateBulletExplosion(i); updateBulletExplosion(i);
return; return;
} }
bullets[i].pos.x += bullets[i].vel.x; bullets[i].pos.x += bullets[i].vel.x - (player.vel.x >> 3);
bullets[i].pos.y += bullets[i].vel.y; bullets[i].pos.y += bullets[i].vel.y - (playerScrollVelY >> 3);
if(bullets[i].pos.x >= GAME_WRAP){ if(bullets[i].pos.x >= GAME_WRAP){
bullets[i].pos.x -= GAME_WRAP; bullets[i].pos.x -= GAME_WRAP;
@ -203,7 +212,7 @@ static void updateBullet(u8 i){
return; return;
} }
if(offScreenBottom){ if(offScreenBottom){
killBullet(i, TRUE); killBullet(i, FALSE);
return; return;
} }
if(bullets[i].clock > 0) bullets[i].updater(i); if(bullets[i].clock > 0) bullets[i].updater(i);
@ -211,8 +220,8 @@ static void updateBullet(u8 i){
else if(!gameOver) collideWithPlayer(i); else if(!gameOver) collideWithPlayer(i);
if(bullets[i].active){ if(bullets[i].active){
s16 sx = getScreenX(bullets[i].pos.x, player.camera); s16 sx = getScreenX(bullets[i].pos.x, player.camera);
s16 sy = fix32ToInt(bullets[i].pos.y); s16 sy = F32_toInt(bullets[i].pos.y);
u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF; u8 off = BULLET_OFF;
SPR_setPosition(bullets[i].image, sx - off, sy - off); SPR_setPosition(bullets[i].image, sx - off, sy - off);
bullets[i].clock++; bullets[i].clock++;
bulletCount++; bulletCount++;
@ -225,7 +234,7 @@ void updateBullets(){
if(killBullets){ if(killBullets){
killBullets = FALSE; killBullets = FALSE;
for(s16 i = 0; i < BULLET_COUNT; i++) for(s16 i = 0; i < BULLET_COUNT; i++)
if(bullets[i].active && !bullets[i].player) killBullet(i, TRUE); if(bullets[i].active && !bullets[i].player && !bullets[i].explosion) killBullet(i, TRUE);
} }
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active)
updateBullet(i); updateBullet(i);

View file

@ -1,17 +1,20 @@
#define MAP_I 512 #define MAP_I 328
#define MAP_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I) #define MAP_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I)
#define MAP_PLAYER_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 1) #define MAP_PLAYER_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 1)
#define MAP_ENEMY_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 2) #define MAP_ENEMY_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 2)
#define MAP_TREASURE_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3) #define MAP_BOSS_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 9)
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4) #define MAP_TREASURE_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 3)
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 4)
#define FONT_BIG_I 256 u16 hudPal = PAL0;
#define FONT_BIG_I 340
void bigText(char* str, u16 x, u16 y, bool shadow){ void bigText(char* str, u16 x, u16 y, bool shadow){
for(u8 i = 0; i < strlen(str); i++){ for(u8 i = 0; i < strlen(str); i++){
if(str[i] >= 48){ if(str[i] >= 48){
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + str[i] - 48), x + i, y); VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + str[i] - 48), x + i, y);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + 16 + str[i] - 48), x + i, y + 1); VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + 16 + str[i] - 48), x + i, y + 1);
} }
} }
} }
@ -31,7 +34,7 @@ s16 lastLives;
static void drawLives(){ static void drawLives(){
VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16); VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16);
for(u8 i = 0; i < (player.lives - 1); i++) for(u8 i = 0; i < (player.lives - 1); i++)
VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, LIFE_I + (i > 0 ? 2 : 0)), LIVES_X, LIVES_Y + i, 1, 2); VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, LIFE_I + (i > 0 ? 2 : 0)), LIVES_X, LIVES_Y + i, 1, 2);
lastLives = player.lives; lastLives = player.lives;
} }
@ -57,17 +60,17 @@ static void drawScore(){
void loadMap(){ void loadMap(){
VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H); VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1); VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1);
for(s16 i = 0; i < ENEMY_COUNT; i++){ for(s16 i = 0; i < ENEMY_COUNT; i++){
mapEnemyCol[i] = -1; mapEnemyCol[i] = -1;
@ -96,7 +99,7 @@ static bool mapTileOccupied(s16 col, s16 row, s16 pRow){
static void updateMap(){ static void updateMap(){
// compute new player row // compute new player row
s16 pRow = fix32ToInt(player.pos.y) / 75; s16 pRow = F32_toInt(player.pos.y) / 75;
if(pRow < 0) pRow = 0; if(pRow < 0) pRow = 0;
if(pRow >= MAP_H) pRow = MAP_H - 1; if(pRow >= MAP_H) pRow = MAP_H - 1;
@ -109,10 +112,10 @@ static void updateMap(){
continue; continue;
} }
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x); fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
s16 col = fix32ToInt(dx) / 54 + MAP_W / 2; s16 col = F32_toInt(dx) / MAP_SCALE + MAP_W / 2;
if(col < 0) col = 0; if(col < 0) col = 0;
if(col >= MAP_W) col = MAP_W - 1; if(col >= MAP_W) col = MAP_W - 1;
s16 row = fix32ToInt(enemies[i].pos.y) / 75; s16 row = F32_toInt(enemies[i].pos.y) / 75;
if(row < 0) row = 0; if(row < 0) row = 0;
if(row >= MAP_H) row = MAP_H - 1; if(row >= MAP_H) row = MAP_H - 1;
mapNewCol[i] = col; mapNewCol[i] = col;
@ -127,10 +130,10 @@ static void updateMap(){
continue; continue;
} }
fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x); fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x);
s16 col = fix32ToInt(dx) / 54 + MAP_W / 2; s16 col = F32_toInt(dx) / MAP_SCALE + MAP_W / 2;
if(col < 0) col = 0; if(col < 0) col = 0;
if(col >= MAP_W) col = MAP_W - 1; if(col >= MAP_W) col = MAP_W - 1;
s16 row = fix32ToInt(treasures[i].pos.y) / 75; s16 row = F32_toInt(treasures[i].pos.y) / 75;
if(row < 0) row = 0; if(row < 0) row = 0;
if(row >= MAP_H) row = MAP_H - 1; if(row >= MAP_H) row = MAP_H - 1;
mapNewTreasureCol[i] = col; mapNewTreasureCol[i] = col;
@ -173,7 +176,8 @@ static void updateMap(){
mapEnemyRow[i] = mapNewRow[i]; mapEnemyRow[i] = mapNewRow[i];
if(mapNewCol[i] < 0) continue; if(mapNewCol[i] < 0) continue;
if(mapNewCol[i] == MAP_W / 2 && mapNewRow[i] == pRow) continue; if(mapNewCol[i] == MAP_W / 2 && mapNewRow[i] == pRow) continue;
VDP_setTileMapXY(BG_A, MAP_ENEMY_TILE, MAP_X + mapNewCol[i], MAP_Y + mapNewRow[i]); u16 eTile = (enemies[i].type == ENEMY_TYPE_BOSS) ? MAP_BOSS_TILE : MAP_ENEMY_TILE;
VDP_setTileMapXY(BG_A, eTile, MAP_X + mapNewCol[i], MAP_Y + mapNewRow[i]);
} }
// draw player dot on top // draw player dot on top
@ -185,29 +189,68 @@ u8 phraseIndex[4];
s16 lastLevel; s16 lastLevel;
static void drawLevel(){ static void drawLevel(){
if(isAttract) return;
char lvlStr[4]; char lvlStr[4];
uintToStr(level + 1, lvlStr, 1); uintToStr(level + 1, lvlStr, 1);
VDP_drawText("LVL", 1, 8); VDP_setTextPalette(hudPal);
VDP_drawText(lvlStr, 4, 8); // VDP_drawText(lvlStr, 1, 7);
VDP_setTextPalette(PAL0);
lastLevel = level; lastLevel = level;
} }
static void repaintMap(){
VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1);
// redraw tracked dots
for(s16 i = 0; i < TREASURE_COUNT; i++)
if(mapTreasureCol[i] >= 0)
VDP_setTileMapXY(BG_A, MAP_TREASURE_TILE, MAP_X + mapTreasureCol[i], MAP_Y + mapTreasureRow[i]);
for(s16 i = 0; i < ENEMY_COUNT; i++)
if(mapEnemyCol[i] >= 0){
u16 eTile = (enemies[i].type == ENEMY_TYPE_BOSS) ? MAP_BOSS_TILE : MAP_ENEMY_TILE;
VDP_setTileMapXY(BG_A, eTile, MAP_X + mapEnemyCol[i], MAP_Y + mapEnemyRow[i]);
}
if(mapPlayerRow >= 0)
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + mapPlayerRow);
}
static void repaintHud(){
bigText(scoreStr, SCORE_X, SCORE_Y, FALSE);
drawLives();
repaintMap();
drawLevel();
}
void loadChrome(){ void loadChrome(){
VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA); VDP_loadTileSet(imageFontBigger.tileset, FONT_BIG_I, DMA);
VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA); VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA);
VDP_loadTileSet(imageChromeLife.tileset, LIFE_I, DMA); VDP_loadTileSet(imageChromeLife.tileset, LIFE_I, DMA);
VDP_loadTileSet(imageChromeLife2.tileset, LIFE_I + 2, DMA); VDP_loadTileSet(imageChromeLife2.tileset, LIFE_I + 2, DMA);
VDP_loadTileSet(mapIndicator.tileset, MAP_I, DMA); VDP_loadTileSet(mapIndicator.tileset, MAP_I, DMA);
lastScore = 1; lastScore = 1;
drawScore(); if(!isAttract) drawScore();
drawLives(); if(!isAttract) drawLives();
drawLevel(); drawLevel();
} }
bool didGameOver; bool didGameOver;
u32 gameOverClock; u32 gameOverClock;
static bool gameOverFading;
static void doGameOver(){ static void doGameOver(){
didGameOver = TRUE; didGameOver = TRUE;
// check and save high score
if(score > highScore){
highScore = score;
saveHighScore();
VDP_drawText("NEW HIGH SCORE!", 14, 15);
}
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1); for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1); for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active){ for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active){
@ -238,19 +281,28 @@ static void doGameOver(){
treasureCollectedClock = 0; treasureCollectedClock = 0;
allTreasureCollected = FALSE; allTreasureCollected = FALSE;
hitMessageClock = 0; hitMessageClock = 0;
VDP_clearText(9, 5, 22); VDP_clearText(9, 5, 23);
VDP_drawText("GAME OVER", 15, 13); hudPal = PAL1;
VDP_drawText("PRESS ANY BUTTON", 12, 14); hudPal = PAL1;
repaintHud();
VDP_drawText("GAME OVER", 15, 14);
VDP_drawText("Press Any Button", 12, 16);
} }
#define PAUSE_Y 15
static void showPause(){ static void showPause(){
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1); for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1); for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL1); for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL1);
SPR_setPalette(player.image, PAL1); SPR_setPalette(player.image, PAL1);
hudPal = PAL1;
hudPal = PAL1;
repaintHud();
XGM2_pause(); XGM2_pause();
VDP_drawText("PAUSE", 17, 13); VDP_drawText("PAUSED", 17, PAUSE_Y);
} }
static void clearPause(){ static void clearPause(){
@ -258,13 +310,15 @@ static void clearPause(){
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0); for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0); for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0);
SPR_setPalette(player.image, PAL0); SPR_setPalette(player.image, PAL0);
hudPal = PAL0;
repaintHud();
XGM2_resume(); XGM2_resume();
VDP_clearText(17, 13, 5); VDP_clearText(17, PAUSE_Y, 6);
} }
u32 pauseClock; u32 pauseClock;
static void updatePause(){ static void updatePause(){
if(gameOver) return; if(gameOver || isAttract || levelWaitClock > 0 || levelClearing) return;
if(ctrl.start){ if(ctrl.start){
if(!isPausing){ if(!isPausing){
isPausing = TRUE; isPausing = TRUE;
@ -282,59 +336,90 @@ static void updatePause(){
} }
if(paused){ if(paused){
if(pauseClock % 60 < 30) if(pauseClock % 60 < 30)
VDP_drawText("PAUSE", 17, 13); VDP_drawText("PAUSED", 17, PAUSE_Y);
else else
VDP_clearText(17, 13, 5); VDP_clearText(17, PAUSE_Y, 6);
pauseClock++; pauseClock++;
if(pauseClock >= 240) pauseClock = 0; if(pauseClock >= 240) pauseClock = 0;
} }
} }
#define TRANSITION_TREASURE_X 10
#define TRANSITION_TREASURE_Y 13
#define TRANSITION_LEVEL_X 12
#define TRANSITION_LEVEL_Y 15
void updateChrome(){ void updateChrome(){
updatePause(); updatePause();
if(gameOver && !didGameOver) doGameOver(); if(gameOver && !didGameOver) doGameOver();
if(didGameOver){ if(didGameOver){
gameOverClock++; gameOverClock++;
if((gameOverClock > 120 && (ctrl.a || ctrl.b || ctrl.c || ctrl.start)) || gameOverClock > 900) if(!gameOverFading){
if((gameOverClock > 120 && (ctrl.a || ctrl.b || ctrl.c || ctrl.start)) || gameOverClock > 900){
gameOverFading = TRUE;
PAL_fadeOut(0, 47, 20, TRUE);
}
} else if(!PAL_isDoingFade()){
SYS_hardReset(); SYS_hardReset();
}
return; return;
} }
// level transition overlay // level transition overlay
if(levelClearing){ if(levelClearing){
if(levelClearClock == 2){ if(levelClearClock == 2){
char numStr[12];
char lvlStr[4]; char lvlStr[4];
uintToStr(level + 2, lvlStr, 1); char livesStr[4];
VDP_drawText("LEVEL ", 15, 13); score += 2048 + 1024 * level;
VDP_drawText(lvlStr, 21, 13); lastScore = score;
uintToStr(statTreasures, numStr, 1);
VDP_drawText("Collected", TRANSITION_TREASURE_X, TRANSITION_TREASURE_Y);
VDP_drawText(numStr, TRANSITION_TREASURE_X + 10, TRANSITION_TREASURE_Y);
VDP_drawText("Treasure", TRANSITION_TREASURE_X + 10 + 2, TRANSITION_TREASURE_Y);
if(statTreasures != 1) VDP_drawText("s", TRANSITION_TREASURE_X + 10 + 2 + 8, TRANSITION_TREASURE_Y);
uintToStr(level + 1, lvlStr, 1);
VDP_drawText("Completed Level", TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y);
VDP_drawText(lvlStr, TRANSITION_LEVEL_X + 16, TRANSITION_LEVEL_Y);
uintToStr(lastScore, scoreStr, 1);
VDP_drawText("Score", TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y + 3);
VDP_drawText(scoreStr, TRANSITION_LEVEL_X + 6, TRANSITION_LEVEL_Y + 3);
uintToStr(player.lives, livesStr, 1);
VDP_drawText(livesStr, TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y + 5);
if(player.lives == 1)
VDP_drawText("Life Left", TRANSITION_LEVEL_X + 2, TRANSITION_LEVEL_Y + 5);
else
VDP_drawText("Lives Left", TRANSITION_LEVEL_X + 2, TRANSITION_LEVEL_Y + 5);
} }
if(levelClearClock >= 110){ if(levelClearClock >= 230){
VDP_clearText(15, 13, 10); VDP_clearText(0, TRANSITION_TREASURE_Y, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 3, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 5, 40);
} }
return; return;
} }
if(lastScore != score){ if(!isAttract && lastScore != score){
lastScore = score; lastScore = score;
drawScore(); drawScore();
// check for extend
while(score >= nextExtendScore){
player.lives++;
nextExtendScore = (nextExtendScore * 5) / 2; // previous + previous * 1.5
drawLives();
}
} }
if(lastLives != player.lives) drawLives(); if(!isAttract && lastLives != player.lives) drawLives();
if(lastLevel != level) drawLevel(); if(lastLevel != level) drawLevel();
if(treasureCollectedClock > 0 && levelWaitClock == 0){ if(treasureCollectedClock > 0 && levelWaitClock == 0){
if(treasureCollectedClock == 120){ if(treasureCollectedClock == 120){
VDP_clearText(10, 5, 22); VDP_clearText(10, 5, 23);
const char* mirrorPhrases[] = {"REFLECT THE DEPTHS", "DIG DEEPER WITHIN", "SEE WHAT SHINES BELOW", "MIRROR OF THE MINE", "LOOK BACK STRIKE BACK"}; // check if all treasures are now collected or gone
const char* lampPhrases[] = {"STRIKE LIGHT", "LET THERE BE LODE", "BRIGHT IDEA DEEP DOWN", "ILLUMINATE THE VEIN", "GLOW FROM BELOW"};
const char* scarfPhrases[] = {"COZY IN THE CAVES", "WRAP THE UNDERWORLD", "SNUG AS BEDROCK", "STYLE FROM THE STRATA", "WARM THE DEPTHS"};
const char* swordPhrases[] = {"ORE YOU READY", "MINED YOUR STEP", "CUTTING EDGE GEOLOGY", "STRIKE THE VEIN", "SPIRIT STEEL"};
const char** sets[] = {mirrorPhrases, lampPhrases, scarfPhrases, swordPhrases};
const char* phrase = sets[treasureCollectedType][phraseIndex[treasureCollectedType]];
phraseIndex[treasureCollectedType] = (phraseIndex[treasureCollectedType] + 1) % 5;
u8 len = strlen(phrase);
VDP_drawText(phrase, 20 - len / 2, 5);
}
treasureCollectedClock--;
if(treasureCollectedClock == 0){
VDP_clearText(10, 5, 22);
// check if all treasures are collected or gone
bool allDone = TRUE; bool allDone = TRUE;
for(s16 j = 0; j < TREASURE_COUNT; j++){ for(s16 j = 0; j < TREASURE_COUNT; j++){
if(treasures[j].active && treasures[j].state != TREASURE_COLLECTED){ if(treasures[j].active && treasures[j].state != TREASURE_COLLECTED){
@ -344,26 +429,41 @@ void updateChrome(){
} }
if(allDone && collectedCount > 0){ if(allDone && collectedCount > 0){
allTreasureCollected = TRUE; allTreasureCollected = TRUE;
VDP_drawText("ALL TREASURE COLLECTED", 9, 5); VDP_drawText("All Treasure Found!", 11, 5);
} else {
const char* mirrorPhrases[] = {"Reflect the Depths", "Dig Deeper Within", "See What Shines Below", "Mirror of the Mine", "Look Back, Strike Back"};
const char* lampPhrases[] = {"Strike Light", "Let There Be Lode!", "Bright Idea Deep Down", "Illuminate the Vein", "Glow from Below"};
const char* scarfPhrases[] = {"Cozy in the Caves", "Wrap the Underworld", "Snug as Bedrock", "Style from the Strata", "Warm the Depths"};
const char* swordPhrases[] = {"Ore You Ready?", "Mined Your Step", "Cutting Edge Geology", "Strike the Vein", "Spirit Steel"};
const char** sets[] = {mirrorPhrases, lampPhrases, scarfPhrases, swordPhrases};
const char* phrase = sets[treasureCollectedType][phraseIndex[treasureCollectedType]];
phraseIndex[treasureCollectedType] = (phraseIndex[treasureCollectedType] + 1) % 5;
u8 len = strlen(phrase);
u8 phraseX = 20 - len / 2;
if(phraseX < 10) phraseX = 10;
VDP_drawText(phrase, phraseX, 5);
} }
} }
treasureCollectedClock--;
if(treasureCollectedClock == 0)
VDP_clearText(9, 5, 24);
} }
if(hitMessageClock > 0){ if(hitMessageClock > 0){
if(hitMessageClock == 120){ if(hitMessageClock == 120){
VDP_clearText(9, 5, 22); VDP_clearText(9, 5, 23);
treasureCollectedClock = 0; treasureCollectedClock = 0;
allTreasureCollected = FALSE; allTreasureCollected = FALSE;
VDP_drawText(hitMessageBullet ? "BLASTED" : "SMASHED", hitMessageBullet ? 16 : 16, 5); VDP_drawText(hitMessageBullet ? "Got You!" : "Collision!", 20 - (hitMessageBullet ? 8 : 10) / 2, 5);
} }
hitMessageClock--; hitMessageClock--;
if(hitMessageClock == 0) if(hitMessageClock == 0)
VDP_clearText(9, 5, 22); VDP_clearText(9, 5, 23);
} }
if(levelWaitClock == 240){ if(levelWaitClock == 210){
VDP_clearText(9, 5, 22); VDP_clearText(9, 5, 23);
treasureCollectedClock = 0; treasureCollectedClock = 0;
allTreasureCollected = FALSE; allTreasureCollected = FALSE;
VDP_drawText("ALL ENEMIES DESTROYED", 9, 5); VDP_drawText("All Enemies Down!", 12, 5);
} }
if(clock % 4 == 0) updateMap(); if(clock % 4 == 0) updateMap();
} }

View file

@ -42,19 +42,27 @@ void spawnEnemy(u8 type, u8 zone){
enemies[i].pos.x = randX; enemies[i].pos.x = randX;
enemies[i].pos.y = randY; enemies[i].pos.y = randY;
enemies[i].off = 16; static const SpriteDefinition* bossSpriteDefs[4] = { &boss1Sprite, &boss2Sprite, &boss3Sprite, &boss4Sprite };
enemies[i].image = SPR_addSprite(&fairySprite, SpriteDefinition const* spriteDef;
getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, fix32ToInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0)); if(type == ENEMY_TYPE_DRONE) spriteDef = &eyeBigSprite;
else if(type == ENEMY_TYPE_BOSS) spriteDef = bossSpriteDefs[pendingBossNum % 4];
else spriteDef = &fairySprite;
enemies[i].off = (type == ENEMY_TYPE_BOSS) ? 24 : 16;
enemies[i].image = SPR_addSprite(spriteDef,
getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, F32_toInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0));
if(!enemies[i].image){ if(!enemies[i].image){
enemies[i].active = FALSE; enemies[i].active = FALSE;
return; return;
} }
SPR_setDepth(enemies[i].image, (type == ENEMY_TYPE_BOSS) ? 1 : 2);
SPR_setVisibility(enemies[i].image, HIDDEN); SPR_setVisibility(enemies[i].image, HIDDEN);
enemies[i].hp = 1; enemies[i].hp = 1;
for(u8 j = 0; j < PROP_COUNT; j++){ for(u8 j = 0; j < PROP_COUNT; j++){
enemies[i].ints[j] = 0; enemies[i].ints[j] = 0;
enemies[i].fixes[j] = 0;
} }
enemies[i].ints[3] = -1; enemies[i].ints[3] = -1;
enemies[i].anim = 0;
switch(enemies[i].type){ switch(enemies[i].type){
case ENEMY_TYPE_TEST: case ENEMY_TYPE_TEST:
loadEnemyOne(i); loadEnemyOne(i);
@ -75,9 +83,9 @@ void spawnEnemy(u8 type, u8 zone){
loadBoss(i); loadBoss(i);
break; break;
} }
enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
SPR_setAnim(enemies[i].image, enemies[i].anim);
} }
static void boundsEnemy(u8 i){ static void boundsEnemy(u8 i){
@ -92,21 +100,39 @@ static void boundsEnemy(u8 i){
} }
// carrying: only check for reaching the top // carrying: only check for reaching the top
else if(enemies[i].pos.y <= FIX32(0)){ else if(enemies[i].pos.y <= FIX32(0)){
if(treasures[h].active) killTreasure(h); if(isAttract){
enemies[i].ints[3] = -1; // in attract mode enemies can't die -- drop treasure and head back down
treasureBeingCarried = FALSE; if(treasures[h].active){
if(enemies[i].type == ENEMY_TYPE_BUILDER){ treasures[h].state = TREASURE_FALLING;
u8 zone = fix32ToInt(enemies[i].pos.x) / 512; treasures[h].carriedBy = -1;
spawnEnemy(ENEMY_TYPE_GUNNER, zone); treasures[h].vel.x = 0;
treasures[h].vel.y = FIX32(3);
}
enemies[i].ints[3] = -1;
treasureBeingCarried = FALSE;
enemies[i].vel.y = FIX32(1);
} else {
if(treasures[h].active) killTreasure(h);
enemies[i].ints[3] = -1;
treasureBeingCarried = FALSE;
if(enemies[i].type == ENEMY_TYPE_BUILDER){
u8 zone = F32_toInt(enemies[i].pos.x) / 512;
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
}
enemies[i].hp = 0;
killEnemy(i);
} }
enemies[i].hp = 0;
killEnemy(i);
return; return;
} }
} else { } else {
// not carrying: bounce off top and bottom // not carrying: bounce off top and bottom
if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off) || enemies[i].pos.y <= FIX32(enemies[i].off)) if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off)){
enemies[i].vel.y *= -1; if(enemies[i].vel.y > 0) enemies[i].vel.y = -enemies[i].vel.y;
enemies[i].pos.y = GAME_H_F - FIX32(enemies[i].off);
} else if(enemies[i].pos.y <= FIX32(enemies[i].off)){
if(enemies[i].vel.y < 0) enemies[i].vel.y = -enemies[i].vel.y;
enemies[i].pos.y = FIX32(enemies[i].off);
}
} }
if(enemies[i].pos.x >= GAME_WRAP){ if(enemies[i].pos.x >= GAME_WRAP){
@ -118,8 +144,8 @@ static void boundsEnemy(u8 i){
} }
static void updateEnemy(u8 i){ static void updateEnemy(u8 i){
enemies[i].pos.x += enemies[i].vel.x; enemies[i].pos.x += enemies[i].vel.x - (player.vel.x >> 3);
enemies[i].pos.y += enemies[i].vel.y; enemies[i].pos.y += enemies[i].vel.y - (playerScrollVelY >> 3);
boundsEnemy(i); boundsEnemy(i);
if(!enemies[i].active) return; if(!enemies[i].active) return;
@ -169,6 +195,7 @@ static void updateEnemy(u8 i){
bullets[expSlot].frame = 0; bullets[expSlot].frame = 0;
bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0)); bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0));
if(bullets[expSlot].image){ if(bullets[expSlot].image){
SPR_setDepth(bullets[expSlot].image, 5);
SPR_setAnim(bullets[expSlot].image, 1); SPR_setAnim(bullets[expSlot].image, 1);
SPR_setFrame(bullets[expSlot].image, 0); SPR_setFrame(bullets[expSlot].image, 0);
SPR_setHFlip(bullets[expSlot].image, random() & 1); SPR_setHFlip(bullets[expSlot].image, random() & 1);
@ -180,24 +207,29 @@ static void updateEnemy(u8 i){
enemies[i].hp = 0; enemies[i].hp = 0;
killEnemy(i); killEnemy(i);
} }
player.lives--; if(!isAttract){
if(player.lives == 0){ player.lives--;
gameOver = TRUE; if(player.lives == 0){
XGM2_stop(); gameOver = TRUE;
} else { XGM2_stop();
player.respawnClock = 120; sfxGameOver();
SPR_setVisibility(player.image, HIDDEN); } else {
killBullets = TRUE; sfxPlayerHit();
hitMessageClock = 120; player.respawnClock = 120;
hitMessageBullet = FALSE; SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = FALSE;
}
} }
} }
} }
s16 sx = getScreenX(enemies[i].pos.x, player.camera); s16 sx = getScreenX(enemies[i].pos.x, player.camera);
s16 sy = fix32ToInt(enemies[i].pos.y); s16 sy = F32_toInt(enemies[i].pos.y);
SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN); SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN);
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0); if(enemies[i].type != ENEMY_TYPE_DRONE && enemies[i].type != ENEMY_TYPE_BOSS)
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0);
SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off); SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off);
enemies[i].clock++; enemies[i].clock++;

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ void sfxEnemyShotB();
void sfxEnemyShotC(); void sfxEnemyShotC();
void sfxExplosion(); void sfxExplosion();
void sfxPickup(); void sfxPickup();
void sfxGraze();
void loadMap(); void loadMap();
void loadGame(); void loadGame();
@ -21,12 +22,34 @@ u32 clock;
#define GAME_WRAP (SECTION_SIZE * SECTION_COUNT) #define GAME_WRAP (SECTION_SIZE * SECTION_COUNT)
#define CULL_LIMIT FIX32(240) #define CULL_LIMIT FIX32(240)
#define SCREEN_LIMIT FIX32(208) // max player-to-screen-edge distance (320 - CAMERA_X)
#define MUSIC_VOLUME 50 // #define MUSIC_VOLUME 50
// #define MUSIC_VOLUME 0 #define MUSIC_VOLUME 0
u32 score; u32 score;
u32 highScore;
u32 tempHighScore;
u32 grazeCount;
u32 nextExtendScore;
#define EXTEND_SCORE 25000
#define SCORE_LENGTH 8 #define SCORE_LENGTH 8
#define GRAZE_RADIUS 16
#define SCORE_SRAM 0x0033
void getHighScore(){
SRAM_enable();
tempHighScore = SRAM_readLong(SCORE_SRAM);
if(tempHighScore > 0 && tempHighScore < 1000000) highScore = tempHighScore;
SRAM_disable();
}
static void saveHighScore(){
SRAM_enable();
SRAM_writeLong(SCORE_SRAM, highScore);
SRAM_disable();
}
#define FIRST_ROTATING_BULLET 3 #define FIRST_ROTATING_BULLET 3
@ -34,12 +57,20 @@ u32 score;
#define MAP_Y 1 #define MAP_Y 1
#define MAP_W 38 #define MAP_W 38
#define MAP_H 3 #define MAP_H 3
#define MAP_SCALE (F32_toInt(GAME_WRAP) / MAP_W)
void EMPTY(s16 i){(void)i;} void EMPTY(s16 i){(void)i;}
bool started; bool started;
bool gameOver; bool gameOver;
bool paused, isPausing; bool paused, isPausing;
bool isAttract;
u16 attractClock;
#define ATTRACT_LIMIT 900 // frames of title idle before attract triggers
#define ATTRACT_DURATION 1800 // 30 seconds of demo gameplay
#define ATTRACT_LEVEL 1 // level index for attract mode (L12: Boss 4, 3 gunners)
#define START_LEVEL 2 // offset added to starting level (0 = normal start)
// #define START_LEVEL 0 // offset added to starting level (0 = normal start)
s16 enemyCount, bulletCount; s16 enemyCount, bulletCount;
u8 level; u8 level;
s16 pendingBossHp; s16 pendingBossHp;
@ -59,6 +90,7 @@ struct controls {
bool left, right, up, down, a, b, c, start; bool left, right, up, down, a, b, c, start;
}; };
struct controls ctrl; struct controls ctrl;
struct controls ctrlHW; // hardware-only copy — never overridden by AI
void updateControls(u16 joy, u16 changed, u16 state){ void updateControls(u16 joy, u16 changed, u16 state){
(void)changed; // Unused parameter (void)changed; // Unused parameter
if(joy == JOY_1){ if(joy == JOY_1){
@ -70,6 +102,7 @@ void updateControls(u16 joy, u16 changed, u16 state){
ctrl.b = (state & BUTTON_B); ctrl.b = (state & BUTTON_B);
ctrl.c = (state & BUTTON_C); ctrl.c = (state & BUTTON_C);
ctrl.start = (state & BUTTON_START); ctrl.start = (state & BUTTON_START);
ctrlHW = ctrl;
} }
} }
@ -79,10 +112,13 @@ struct playerStruct {
Vect2D_f32 pos, vel; Vect2D_f32 pos, vel;
s16 shotAngle; s16 shotAngle;
u8 lives, recoveringClock, respawnClock; u8 lives, recoveringClock, respawnClock;
bool recoverFlash; // TRUE only after death, not on level-start grace
bool pendingShow; // show sprite after next position update (avoids 1-frame position flicker)
fix32 camera; fix32 camera;
Sprite* image; Sprite* image;
}; };
struct playerStruct player; struct playerStruct player;
fix32 playerScrollVelY; // player.vel.y zeroed when clamped at top/bottom bound
bool killBullets; bool killBullets;
@ -93,14 +129,17 @@ struct bulletSpawner {
fix32 x, y, speed; fix32 x, y, speed;
Vect2D_f32 vel; Vect2D_f32 vel;
s16 angle, anim, frame; s16 angle, anim, frame;
s16 ints[PROP_COUNT];
bool top, player; bool top, player;
}; };
struct bullet { struct bullet {
bool active, player, vFlip, hFlip, explosion; fix32 speed;
bool active, player, vFlip, hFlip, explosion, grazed;
Vect2D_f32 pos, vel; Vect2D_f32 pos, vel;
Sprite* image; Sprite* image;
s16 clock, angle, anim, frame; s16 clock, angle, anim, frame;
s16 dist; s16 dist;
s16 ints[PROP_COUNT];
void (*updater)(s16); void (*updater)(s16);
}; };
struct bullet bullets[BULLET_COUNT]; struct bullet bullets[BULLET_COUNT];
@ -119,13 +158,14 @@ struct bullet bullets[BULLET_COUNT];
struct enemy { struct enemy {
bool active, onScreen; bool active, onScreen;
u8 type; u8 type;
s16 hp; s16 hp, frame, anim;
s16 angle, off; s16 angle, off;
u32 clock; u32 clock;
fix32 speed; fix32 speed;
Vect2D_f32 vel, pos; Vect2D_f32 vel, pos;
Sprite* image; Sprite* image;
s16 ints[PROP_COUNT]; s16 ints[PROP_COUNT];
fix16 fixes[PROP_COUNT];
}; };
struct enemy enemies[ENEMY_COUNT]; struct enemy enemies[ENEMY_COUNT];
@ -148,6 +188,9 @@ struct treasure {
struct treasure treasures[TREASURE_COUNT]; struct treasure treasures[TREASURE_COUNT];
bool treasureBeingCarried; bool treasureBeingCarried;
s16 collectedCount; s16 collectedCount;
u16 levelEnemiesKilled;
u16 statEnemiesKilled;
s16 statTreasures;
void killTreasure(u8 i){ void killTreasure(u8 i){
if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){ if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){
@ -160,19 +203,17 @@ void killTreasure(u8 i){
void killBullet(u8 i, bool explode){ void killBullet(u8 i, bool explode){
if(explode){ if(explode){
s16 a = bullets[i].anim;
s16 explosionAnim;
if(bullets[i].player){ if(bullets[i].player){
SPR_setAnim(bullets[i].image, 1); explosionAnim = 16;
} else if(a < FIRST_ROTATING_BULLET){
explosionAnim = 13 + bullets[i].frame;
} else { } else {
s16 a = bullets[i].anim; s16 mod = a % 3;
s16 explosionAnim; explosionAnim = 13 + mod;
if(a < FIRST_ROTATING_BULLET){
explosionAnim = 13 + bullets[i].frame;
} else {
s16 mod = a % 3;
explosionAnim = 13 + mod;
}
SPR_setAnim(bullets[i].image, explosionAnim);
} }
SPR_setAnim(bullets[i].image, explosionAnim);
bullets[i].clock = 0; bullets[i].clock = 0;
bullets[i].frame = 0; bullets[i].frame = 0;
bullets[i].explosion = TRUE; bullets[i].explosion = TRUE;
@ -186,6 +227,7 @@ void killBullet(u8 i, bool explode){
} }
void killEnemy(u8 i){ void killEnemy(u8 i){
if(isAttract) return;
enemies[i].hp--; enemies[i].hp--;
if(enemies[i].hp > 0) return; if(enemies[i].hp > 0) return;
if(enemies[i].ints[3] >= 0){ if(enemies[i].ints[3] >= 0){
@ -200,6 +242,7 @@ void killEnemy(u8 i){
} }
enemies[i].active = FALSE; enemies[i].active = FALSE;
SPR_releaseSprite(enemies[i].image); SPR_releaseSprite(enemies[i].image);
levelEnemiesKilled++;
} }
static fix32 getWrappedDelta(fix32 a, fix32 b) { static fix32 getWrappedDelta(fix32 a, fix32 b) {
@ -214,37 +257,61 @@ static fix32 getWrappedDelta(fix32 a, fix32 b) {
static s16 getScreenX(fix32 worldX, fix32 camera) { static s16 getScreenX(fix32 worldX, fix32 camera) {
fix32 screenX = worldX - camera; fix32 screenX = worldX - camera;
if (screenX < FIX32(-256)) { if (screenX < -(GAME_WRAP / 2)) {
screenX += GAME_WRAP; screenX += GAME_WRAP;
} else if (screenX > FIX32(256)) { } else if (screenX > (GAME_WRAP / 2)) {
screenX -= GAME_WRAP; screenX -= GAME_WRAP;
} }
return fix32ToInt(screenX); return F32_toInt(screenX);
} }
// homing // homing -- degree-based using SGDK F16_atan2 (returns fix16 degrees)
#define PI_MOD 2.84444444444 static fix16 getAngle(fix32 dx, fix32 dy){
#define PI_F FIX16(3.14159265358 * PI_MOD) s16 ix = (s16)(F32_toInt(dx) >> 2);
#define PI_F_2 FIX16(1.57079632679 * PI_MOD) s16 iy = (s16)(F32_toInt(dy) >> 2);
#define PI_F_4 FIX16(0.78539816339 * PI_MOD) if(ix == 0 && iy == 0) return 0;
fix16 arctan(fix16 x) { return F16_normalizeAngle(F16_atan2(FIX16(iy), FIX16(ix)));
return fix16Mul(PI_F_4, x) - fix16Mul(fix16Mul(x, (abs(x) - 1)), (FIX16(0.245) + fix16Mul(FIX16(0.066), abs(x))));
} }
fix16 arctan2(fix16 y, fix16 x) {
return x >= 0 ? // safe angle accumulation -- keeps angle in [-180, 180) so adding up to 180° can't overflow s16
(y >= 0 ? (y < x ? arctan(fix16Div(y, x)) : PI_F_2 - arctan(fix16Div(x, y))) : (-y < x ? arctan(fix16Div(y, x)) : -PI_F_2 - arctan(fix16Div(x, y)))) : static s16 angleAdd(s16 a, s16 step){
(y >= 0 ? (y < -x ? arctan(fix16Div(y, x)) + PI_F : PI_F_2 - arctan(fix16Div(x, y))) : (-y < -x ? arctan(fix16Div(y, x)) - PI_F : -PI_F_2 - arctan(fix16Div(x, y)))); if(a >= FIX16(180)) a -= FIX16(360);
return a + step;
} }
s16 arcAngle;
s16 honeAngle(fix16 x1, fix16 x2, fix16 y1, fix16 y2){ static bool isBossLevel(u8 lvl){
arcAngle = arctan2(y2 - y1, x2 - x1); return (lvl >= 2) && ((lvl + 1) % 3 == 0);
if(arcAngle >= 128) arcAngle -= 32; }
if(arcAngle >= 384) arcAngle -= 32;
if(arcAngle < 0){ #define FONT_THEME_RED 0
arcAngle = 1024 + arcAngle; #define FONT_THEME_GREEN 1
if(arcAngle < 896) arcAngle += 32; #define FONT_THEME_BLUE 2
if(arcAngle < 640) arcAngle += 32;
u16 fontPal[16];
void loadFontPalette(u8 theme) {
u16 coloredPalette[16];
u8 i;
for(i = 0; i < 16; i++) {
u16 color = font.palette->data[i];
u16 r = color & 0xF;
u16 g = (color >> 4) & 0xF;
u16 b = (color >> 8) & 0xF;
switch(theme) {
case FONT_THEME_GREEN:
coloredPalette[i] = (b << 8) | (r << 4) | g;
break;
case FONT_THEME_BLUE: {
u16 newB = r > b ? r : b;
coloredPalette[i] = (newB << 8) | (g << 4) | (r >> 1);
break;
}
default: // FONT_THEME_RED
coloredPalette[i] = color;
break;
}
} }
return arcAngle; memcpy(fontPal, coloredPalette, 16 * sizeof(u16));
PAL_setPalette(PAL3, coloredPalette, DMA_QUEUE);
VDP_setTextPalette(PAL3);
} }

View file

@ -10,19 +10,32 @@
#include "stage.h" #include "stage.h"
#include "chrome.h" #include "chrome.h"
#include "start.h" #include "start.h"
#include "starfield.h"
#include "sfx.h" #include "sfx.h"
static void loadInternals(){ static void loadInternals(){
JOY_init(); JOY_init();
JOY_setEventHandler(&updateControls); JOY_setEventHandler(&updateControls);
SPR_init();
VDP_setPlaneSize(128, 32, TRUE); VDP_setPlaneSize(128, 32, TRUE);
SPR_init();
VDP_loadFont(font.tileset, DMA); VDP_loadFont(font.tileset, DMA);
PAL_setPalette(PAL0, font.palette->data, DMA); PAL_setPalette(PAL0, font.palette->data, DMA);
PAL_setPalette(PAL1, shadow.palette->data, CPU); PAL_setPalette(PAL1, shadow.palette->data, CPU);
PAL_setPalette(PAL2, shadow.palette->data, CPU);
VDP_setTextPriority(1); VDP_setTextPriority(1);
} }
static bool attractEnding;
static void startLevelFadeIn(){
u16 ft[48];
memcpy(ft, font.palette->data, 16 * sizeof(u16));
memcpy(ft + 16, shadow.palette->data, 16 * sizeof(u16));
memcpy(ft + 32, bgPal, 16 * sizeof(u16));
PAL_setColors(0, palette_black, 48, CPU);
PAL_fadeIn(0, 47, ft, 20, TRUE);
}
void clearLevel(){ void clearLevel(){
for(s16 i = 0; i < BULLET_COUNT; i++) for(s16 i = 0; i < BULLET_COUNT; i++)
if(bullets[i].active) killBullet(i, FALSE); if(bullets[i].active) killBullet(i, FALSE);
@ -34,6 +47,7 @@ void clearLevel(){
collectedCount = 0; collectedCount = 0;
allTreasureCollected = FALSE; allTreasureCollected = FALSE;
treasureCollectedClock = 0; treasureCollectedClock = 0;
levelEnemiesKilled = 0;
// black out everything // black out everything
SPR_setVisibility(player.image, HIDDEN); SPR_setVisibility(player.image, HIDDEN);
VDP_clearTileMapRect(BG_A, 0, 0, 128, 32); VDP_clearTileMapRect(BG_A, 0, 0, 128, 32);
@ -41,43 +55,90 @@ void clearLevel(){
} }
void loadGame(){ void loadGame(){
VDP_setVerticalScroll(BG_A, 0);
score = 0;
nextExtendScore = EXTEND_SCORE;
loadBackground(); loadBackground();
loadPlayer(); loadPlayer();
loadChrome(); loadChrome();
loadLevel(0); loadLevel(isAttract ? ATTRACT_LEVEL : START_LEVEL);
XGM2_play(stageMusic); #if MUSIC_VOLUME > 0
XGM2_setFMVolume(MUSIC_VOLUME); if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic);
XGM2_setPSGVolume(MUSIC_VOLUME); #endif
player.recoveringClock = 240; player.recoveringClock = 240;
player.recoverFlash = FALSE;
killBullets = TRUE; killBullets = TRUE;
attractEnding = FALSE;
started = TRUE; started = TRUE;
startLevelFadeIn();
} }
static void updateGame(){ static void updateGame(){
if(isAttract){
if(!attractEnding){
if(ctrlHW.start || ctrlHW.a || ctrlHW.b || ctrlHW.c){
attractEnding = TRUE;
PAL_fadeOut(0, 47, 20, TRUE);
}
if(attractClock > 0){
attractClock--;
if(attractClock == 20)
PAL_fadeOut(0, 47, 20, TRUE);
} else {
SYS_hardReset();
}
} else if(!PAL_isDoingFade()){
SYS_hardReset();
}
}
updateChrome(); updateChrome();
updateSfx();
if(levelClearing){ if(levelClearing){
levelClearClock++; levelClearClock++;
if(levelClearClock == 73)
XGM2_stop();
if(levelClearClock == 1){ if(levelClearClock == 1){
clearLevel(); clearLevel();
loadStarfield(level % 3);
u16 transPal[32];
memcpy(transPal, font.palette->data, 16 * sizeof(u16));
memcpy(transPal + 16, shadow.palette->data, 16 * sizeof(u16));
PAL_fadeIn(0, 31, transPal, 20, TRUE);
} }
if(levelClearClock >= 120){ if(levelClearClock == 220)
PAL_fadeOut(0, 31, 20, TRUE);
if(levelClearClock >= 240){
levelClearing = FALSE; levelClearing = FALSE;
player.pos.y = FIX32(112); player.pos.y = FIX32(112);
player.camera = player.pos.x - FIX32(160); player.camera = player.pos.x - FIX32(160);
playerVelX = 0; playerVelX = 0;
clearStarfield();
loadBackground(); loadBackground();
loadChrome(); loadChrome();
loadLevel(level + 1); loadLevel(level + 1);
SPR_setVisibility(player.image, VISIBLE); startLevelFadeIn();
player.pendingShow = TRUE;
player.recoveringClock = 240; player.recoveringClock = 240;
player.recoverFlash = FALSE;
killBullets = TRUE; killBullets = TRUE;
XGM2_stop();
#if MUSIC_VOLUME > 0
if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic);
#endif
} }
if(levelClearing) updateStarfield();
return; return;
} }
if(levelWaitClock > 0){ if(levelWaitClock > 0){
levelWaitClock--; levelWaitClock--;
#if MUSIC_VOLUME > 0
if(levelWaitClock == 209 && !isAttract)
XGM2_play(treasureMusic);
#endif
if(levelWaitClock == 20)
PAL_fadeOut(0, 47, 20, TRUE);
if(levelWaitClock == 0){ if(levelWaitClock == 0){
statEnemiesKilled = levelEnemiesKilled;
statTreasures = collectedCount;
levelClearing = TRUE; levelClearing = TRUE;
levelClearClock = 0; levelClearClock = 0;
return; return;
@ -93,8 +154,10 @@ static void updateGame(){
gameOver = TRUE; gameOver = TRUE;
XGM2_stop(); XGM2_stop();
} else { } else {
levelWaitClock = 240; levelWaitClock = 210;
killBullets = TRUE; killBullets = TRUE;
if(paused){ paused = FALSE; clearPause(); }
XGM2_stop();
} }
} }
updateTreasures(); updateTreasures();

View file

@ -1,4 +1,4 @@
#define PLAYER_SPEED FIX32(6) #define PLAYER_SPEED FIX32(5.5)
#define PLAYER_ACCEL PLAYER_SPEED >> 4 #define PLAYER_ACCEL PLAYER_SPEED >> 4
@ -6,10 +6,11 @@
#define PLAYER_BOUND_Y FIX32(PLAYER_OFF) #define PLAYER_BOUND_Y FIX32(PLAYER_OFF)
#define PLAYER_BOUND_H FIX32(224 - PLAYER_OFF) #define PLAYER_BOUND_H FIX32(224 - PLAYER_OFF)
#define CAMERA_X FIX32(96) #define CAMERA_X FIX32(112)
#define CAMERA_W FIX32(224) #define CAMERA_W FIX32(208)
#define SHOT_INTERVAL 15 #define SHOT_INTERVAL 20
#define PLAYER_SHOT_SPEED FIX32(18)
s16 shotClock; s16 shotClock;
fix32 screenX; fix32 screenX;
@ -24,7 +25,7 @@ static void movePlayer(){
if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){ if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){
playerSpeed = PLAYER_SPEED; playerSpeed = PLAYER_SPEED;
if(ctrl.left || ctrl.right){ if(ctrl.left || ctrl.right){
if(!ctrl.a) player.shotAngle = ctrl.left ? 512 : 0; if(!ctrl.a) player.shotAngle = ctrl.left ? FIX16(180) : 0;
targetVelX = ctrl.left ? -playerSpeed : playerSpeed; targetVelX = ctrl.left ? -playerSpeed : playerSpeed;
} }
@ -43,8 +44,8 @@ static void movePlayer(){
player.vel.x = playerVelX; player.vel.x = playerVelX;
if(player.vel.x != 0 && player.vel.y != 0){ if(player.vel.x != 0 && player.vel.y != 0){
player.vel.x = fix32Mul(player.vel.x, FIX32(0.707)); player.vel.x = F32_mul(player.vel.x, FIX32(0.707));
player.vel.y = fix32Mul(player.vel.y, FIX32(0.707)); player.vel.y = F32_mul(player.vel.y, FIX32(0.707));
} }
player.pos.x += player.vel.x; player.pos.x += player.vel.x;
@ -62,10 +63,14 @@ static void movePlayer(){
} }
static void boundsPlayer(){ static void boundsPlayer(){
if(player.pos.y < PLAYER_BOUND_Y) playerScrollVelY = player.vel.y;
if(player.pos.y < PLAYER_BOUND_Y){
player.pos.y = PLAYER_BOUND_Y; player.pos.y = PLAYER_BOUND_Y;
else if(player.pos.y > PLAYER_BOUND_H) if(playerScrollVelY < 0) playerScrollVelY = 0;
} else if(player.pos.y > PLAYER_BOUND_H){
player.pos.y = PLAYER_BOUND_H; player.pos.y = PLAYER_BOUND_H;
if(playerScrollVelY > 0) playerScrollVelY = 0;
}
if(player.pos.x >= GAME_WRAP){ if(player.pos.x >= GAME_WRAP){
player.pos.x -= GAME_WRAP; player.pos.x -= GAME_WRAP;
@ -89,15 +94,18 @@ static void cameraPlayer(){
static void shootPlayer(){ static void shootPlayer(){
if(ctrl.a && shotClock == 0){ if(ctrl.a && shotClock == 0){
// fix32 bulletVelX = (player.shotAngle == 0 ? PLAYER_SHOT_SPEED : -PLAYER_SHOT_SPEED) + (player.vel.x * 3);
struct bulletSpawner spawner = { struct bulletSpawner spawner = {
.x = player.pos.x, .x = player.pos.x,
.y = player.pos.y, .y = player.pos.y,
.anim = 0, .anim = 12,
.speed = FIX32(24), .speed = PLAYER_SHOT_SPEED,
.angle = player.shotAngle, .angle = player.shotAngle,
.player = TRUE .player = TRUE
}; };
spawner.ints[5] = F32_toInt(player.shotAngle == 0 ? PLAYER_SHOT_SPEED : -PLAYER_SHOT_SPEED);
void updater(s16 i){ void updater(s16 i){
bullets[i].vel.x = FIX32(bullets[i].ints[5]) + (player.vel.x << 2);
if(bullets[i].clock == 4) killBullet(i, TRUE); if(bullets[i].clock == 4) killBullet(i, TRUE);
} }
spawnBullet(spawner, updater); spawnBullet(spawner, updater);
@ -106,6 +114,57 @@ static void shootPlayer(){
} else if(shotClock > 0) shotClock--; } else if(shotClock > 0) shotClock--;
} }
static s16 attractXClock = 0;
static s16 attractXState = 0; // 0=moving, 1=paused
static s16 attractXDir = 1; // 1=right, -1=left
static s16 attractYClock = 0;
static s16 attractTargetY = 112;
static void moveAttract(){
ctrl.left = ctrl.right = ctrl.up = ctrl.down = FALSE;
// X: move toward nearest enemy, pause, repeat
attractXClock--;
if(attractXClock <= 0){
if(attractXState == 0){
// finished moving -- pause
attractXState = 1;
attractXClock = 20 + (random() % 30); // pause 20-49 frames
} else {
// finished pausing -- find nearest enemy and head toward it
attractXState = 0;
attractXClock = 55 + (random() % 55); // move 55-109 frames
attractXDir = 1; // default right if no enemies
fix32 bestDist = FIX32(9999);
for(s16 i = 0; i < ENEMY_COUNT; i++){
if(!enemies[i].active) continue;
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
fix32 dist = dx < 0 ? -dx : dx;
if(dist < bestDist){
bestDist = dist;
attractXDir = (dx >= 0) ? 1 : -1;
}
}
}
}
ctrl.right = (attractXState == 0 && attractXDir == 1);
ctrl.left = (attractXState == 0 && attractXDir == -1);
// Y: pick a new target every 60 frames, steer toward it with a soft dead zone
// Range [48, 176] gives 128px of vertical movement
attractYClock++;
if(attractYClock >= 60){
attractYClock = 0;
attractTargetY = 48 + (random() % 128);
}
fix32 dy = FIX32(attractTargetY) - player.pos.y;
ctrl.down = (dy > FIX32(10));
ctrl.up = (dy < FIX32(-10));
// Auto-shoot every few shot intervals
ctrl.a = ((clock % (SHOT_INTERVAL * 3)) < SHOT_INTERVAL);
}
void loadPlayer(){ void loadPlayer(){
player.shotAngle = 0; player.shotAngle = 0;
player.camera = 0; player.camera = 0;
@ -113,10 +172,12 @@ void loadPlayer(){
player.pos.y = FIX32(112); player.pos.y = FIX32(112);
playerVelX = 0; playerVelX = 0;
player.lives = 3; player.lives = 3;
shotClock = isAttract ? SHOT_INTERVAL : 0;
player.image = SPR_addSprite(&momoyoSprite, player.image = SPR_addSprite(&momoyoSprite,
fix32ToInt(player.pos.x) - PLAYER_OFF, F32_toInt(player.pos.x) - PLAYER_OFF,
fix32ToInt(player.pos.y) - PLAYER_OFF, F32_toInt(player.pos.y) - PLAYER_OFF,
TILE_ATTR(PAL0, 0, 0, 0)); TILE_ATTR(PAL0, 0, 0, 0));
SPR_setDepth(player.image, 0);
} }
void updatePlayer(){ void updatePlayer(){
@ -140,29 +201,35 @@ void updatePlayer(){
player.pos.y += (targetY - player.pos.y) >> 3; player.pos.y += (targetY - player.pos.y) >> 3;
// keep sprite position in sync so it doesn't pop on reappear // keep sprite position in sync so it doesn't pop on reappear
s16 sx = getScreenX(player.pos.x, player.camera); s16 sx = getScreenX(player.pos.x, player.camera);
s16 sy = fix32ToInt(player.pos.y); s16 sy = F32_toInt(player.pos.y);
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
player.respawnClock--; player.respawnClock--;
if(player.respawnClock == 0){ if(player.respawnClock == 0){
SPR_setVisibility(player.image, VISIBLE); SPR_setVisibility(player.image, VISIBLE);
player.recoveringClock = 240; player.recoveringClock = 180;
player.recoverFlash = TRUE;
killBullets = TRUE; killBullets = TRUE;
} }
return; return;
} }
if(player.recoveringClock > 0){ if(player.recoveringClock > 0){
if(player.recoveringClock % 10 == 1) if(player.recoverFlash && player.recoveringClock % 10 == 1)
SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN); SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN);
player.recoveringClock--; player.recoveringClock--;
if(player.recoveringClock == 0) if(player.recoveringClock == 0)
SPR_setVisibility(player.image, VISIBLE); SPR_setVisibility(player.image, VISIBLE);
} }
if(isAttract) moveAttract();
movePlayer(); movePlayer();
boundsPlayer(); boundsPlayer();
cameraPlayer(); cameraPlayer();
shootPlayer(); shootPlayer();
s16 sx = getScreenX(player.pos.x, player.camera); s16 sx = getScreenX(player.pos.x, player.camera);
s16 sy = fix32ToInt(player.pos.y); s16 sy = F32_toInt(player.pos.y);
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
if(player.pendingShow){
SPR_setVisibility(player.image, VISIBLE);
player.pendingShow = FALSE;
}
} }
} }

125
src/sfx.h
View file

@ -1,105 +1,58 @@
static s16 sfxShotClock;
static u16 sfxShotFreq;
#define SFX_VOL 2
void sfxPlayerShot(){ void sfxPlayerShot(){
sfxShotClock = 4; XGM2_playPCM(sfxSamplePlayerShot, sizeof(sfxSamplePlayerShot), SOUND_PCM_CH1);
sfxShotFreq = 150;
PSG_setEnvelope(2, SFX_VOL);
PSG_setFrequency(2, sfxShotFreq);
} }
static s16 sfxEnemyShotClock;
static u16 sfxEnemyShotFreq;
static u8 sfxEnemyShotType;
// high sharp zap - quick descending chirp
void sfxEnemyShotA(){ void sfxEnemyShotA(){
if(player.recoveringClock > 0) return; if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 3; XGM2_playPCM(sfxSampleBullet1, sizeof(sfxSampleBullet1), SOUND_PCM_CH2);
sfxEnemyShotFreq = 1200;
sfxEnemyShotType = 0;
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
} }
// mid buzzy pulse - sits in the midrange
void sfxEnemyShotB(){ void sfxEnemyShotB(){
if(player.recoveringClock > 0) return; if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 5; XGM2_playPCM(sfxSampleBullet2, sizeof(sfxSampleBullet2), SOUND_PCM_CH2);
sfxEnemyShotFreq = 400;
sfxEnemyShotType = 1;
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
} }
// quick rising ping - sweeps upward
void sfxEnemyShotC(){ void sfxEnemyShotC(){
if(player.recoveringClock > 0) return; if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 4; XGM2_playPCM(sfxSampleBullet3, sizeof(sfxSampleBullet3), SOUND_PCM_CH2);
sfxEnemyShotFreq = 300;
sfxEnemyShotType = 2;
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
} }
static s16 sfxPickupClock;
static u16 sfxPickupFreq;
void sfxPickup(){
sfxPickupClock = 12;
sfxPickupFreq = 800;
PSG_setEnvelope(0, SFX_VOL);
PSG_setFrequency(0, sfxPickupFreq);
}
static s16 sfxExpClock;
void sfxExplosion(){ void sfxExplosion(){
sfxExpClock = 18; XGM2_playPCM(sfxSampleExplosion, sizeof(sfxSampleExplosion), SOUND_PCM_CH3);
PSG_setNoise(PSG_NOISE_TYPE_WHITE, PSG_NOISE_FREQ_CLOCK2);
PSG_setEnvelope(3, SFX_VOL);
} }
void updateSfx(){ void sfxPickup(){
if(sfxExpClock > 0){ XGM2_playPCM(sfxSamplePickup, sizeof(sfxSamplePickup), SOUND_PCM_CH1);
sfxExpClock--; }
PSG_setEnvelope(3, SFX_VOL + (18 - sfxExpClock) * (15 - SFX_VOL) / 18);
if(sfxExpClock == 0){ void sfxGraze(){
PSG_setEnvelope(3, 15); XGM2_playPCM(sfxSampleGraze, sizeof(sfxSampleGraze), SOUND_PCM_CH1);
} }
}
if(sfxEnemyShotClock > 0){ void sfxStartGame(){
sfxEnemyShotClock--; XGM2_playPCM(sfxSampleStartGame, sizeof(sfxSampleStartGame), SOUND_PCM_CH1);
if(sfxEnemyShotType == 0) sfxEnemyShotFreq -= 300; }
else if(sfxEnemyShotType == 1) sfxEnemyShotFreq -= 50;
else sfxEnemyShotFreq += 150; void sfxPlayerHit(){
PSG_setFrequency(1, sfxEnemyShotFreq); XGM2_playPCM(sfxSamplePlayerHit, sizeof(sfxSamplePlayerHit), SOUND_PCM_CH3);
PSG_setEnvelope(1, SFX_VOL + (sfxEnemyShotType == 0 ? (3 - sfxEnemyShotClock) * (15 - SFX_VOL) / 3 : }
sfxEnemyShotType == 1 ? (5 - sfxEnemyShotClock) * (15 - SFX_VOL) / 5 :
(4 - sfxEnemyShotClock) * (15 - SFX_VOL) / 4)); void sfxGameOver(){
if(sfxEnemyShotClock == 0){ XGM2_playPCM(sfxSampleGameOver, sizeof(sfxSampleGameOver), SOUND_PCM_CH1);
PSG_setEnvelope(1, 15); }
}
} void sfxMenuSelect(){
if(sfxPickupClock > 0){ XGM2_playPCM(sfxSampleMenuSelect, sizeof(sfxSampleMenuSelect), SOUND_PCM_CH2);
sfxPickupClock--; }
// rising staircase: jump up every 3 frames
if(sfxPickupClock % 3 == 0) sfxPickupFreq += 200; void sfxMenuChoose(){
PSG_setFrequency(0, sfxPickupFreq); XGM2_playPCM(sfxSampleMenuChoose, sizeof(sfxSampleMenuChoose), SOUND_PCM_CH2);
PSG_setEnvelope(0, SFX_VOL); }
if(sfxPickupClock == 0){
PSG_setEnvelope(0, 15); void sfxCollectTreasure(){
} XGM2_playPCM(sfxSampleCollectTreasure, sizeof(sfxSampleCollectTreasure), SOUND_PCM_CH3);
} }
if(sfxShotClock > 0){
sfxShotClock--; void sfxCollectAllTreasures(){
sfxShotFreq -= 30; XGM2_playPCM(sfxSampleCollectAllTreasures, sizeof(sfxSampleCollectAllTreasures), SOUND_PCM_CH3);
PSG_setFrequency(2, sfxShotFreq);
PSG_setEnvelope(2, SFX_VOL + (4 - sfxShotClock) * (15 - SFX_VOL) / 4);
if(sfxShotClock == 0){
PSG_setEnvelope(2, 15);
}
}
} }

View file

@ -1,176 +1,191 @@
// ============================================================================= // =============================================================================
// LEVEL DESIGN GUIDE // THREAT POINT LEVEL GENERATION SYSTEM
// ============================================================================= // =============================================================================
// //
// Each level is a single struct defining what spawns. The level ends when all // Levels are procedurally generated using a threat point (TP) budget.
// enemies are killed (enemyCount == 0). Treasures are bonus -- they don't affect // Each enemy type has a TP cost, and the budget grows with level index.
// level completion. // Enemy types unlock progressively. Compositions vary each playthrough
// (RNG seeded from title screen).
// //
// --- STRUCT FIELDS --- // Boss levels occur every 3rd level (indices 2, 5, 8, 11, 14).
// // Boss escorts use 40% of normal budget, limited to drones + builders.
// drones Number of Drone enemies (type 1). Bulk pressure enemy.
// Speed 2, homes toward player every 30 frames.
// Fires 1 aimed bullet every 40 frames (only on L2+, i.e. index >= 1).
// Use dronesShoot=FALSE on L1 to introduce them without bullets.
// Good range: 6-16. Above 14 gets chaotic.
//
// gunners Number of Gunner enemies (type 2). Danmaku / bullet geometry.
// Speed 0.5 (slow drift), only shoots when on screen.
// Pattern controlled by gunnerPattern field (see below).
// Good range: 0-6. Even 2-3 gunners create significant bullet density.
//
// hunters Number of Hunter enemies (type 3). Fast chaser, no shooting.
// Speed 5 (matches player!). Homes every frame. Pure body pressure.
// Very dangerous -- 2-3 is oppressive, 6 is near-impossible.
// Good range: 0-6. Introduce after players learn movement.
//
// builders Number of Builder enemies (type 4). Treasure abductor.
// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking treasures
// and flies upward. If it reaches the top, the treasure dies and a
// Gunner spawns in its place. Only 1 treasure can be carried at a time.
// Creates urgency -- player must choose between killing enemies
// and saving treasures. Good range: 0-2.
//
// bossHp If > 0, spawns a Boss enemy (type 5) with this many HP.
// Boss number is auto-calculated from level index (lvl / 4).
// Set to 0 for non-boss levels. Boss speed is 1, bounces around.
// Boss has multiple attack phases based on remaining HP.
// Typical values: 25, 50, 75, 100, 125.
// Other enemies can coexist with the boss (adds pressure).
//
// treasures Number of treasures to spawn. Distributed across 4 zones
// (2 per zone if >= 4 treasures, then 1 each for remainder).
// Walk along the ground, can be collected by player for 1000 pts
// (2000 if caught mid-fall after enemy drops them).
// Max 8 (TREASURE_COUNT). Usually just set to 8.
//
// gunnerPattern Controls what bullet pattern gunners use:
// 0 = Radial Burst: 8 bullets in a circle every 60 frames.
// Rotating start angle. Steady, predictable pressure.
// 1 = Aimed Fan: 5 bullets aimed at player, spread +-64,
// every 45 frames. More targeted/aggressive.
// 2 = Mix: each gunner randomly picks 0 or 1 at spawn.
// Creates varied, less predictable bullet fields.
//
// dronesShoot TRUE = drones fire aimed bullets (normal behavior on L2+).
// FALSE = drones still home toward player but never shoot.
// Only meaningful for the very first level as a gentle intro.
// (Drone shooting is also gated by level >= 1 in code, so
// L1 drones never shoot regardless of this flag.)
//
// --- LIMITS ---
//
// Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss
// must not exceed 24. If it does, excess enemies silently fail to spawn.
//
// Total treasures: 8 slots (TREASURE_COUNT).
//
// Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets
// get priority and evict enemy bullets when full.
//
// --- SPAWNING ---
//
// Enemies are distributed across 4 zones (each 512px of the 2048px world).
// Enemy i spawns in zone (i % 4). They never spawn within 240px of the player
// and maintain 64px minimum spacing from each other.
//
// Boss always spawns in zone 1.
//
// --- DESIGN TIPS ---
//
// - Drone-heavy levels (12-16) create constant movement pressure
// - Gunner-heavy levels (4-6) create bullet reading / dodging challenges
// - Hunter levels force the player to keep moving (anti-camping)
// - Builder levels force tough choices: kill builders or save treasures?
// - Combining hunters + gunners is very hard (dodge bullets while fleeing)
// - Boss levels with escort enemies (drones/gunners alongside boss) are
// harder than solo boss fights
// - A "farm" level (lots of drones, no gunners) gives score-building breathers
// - gunnerPattern 0 (radial) is easier to dodge than 1 (aimed fan)
// //
// ============================================================================= // =============================================================================
struct LevelDef { #define LEVEL_COUNT 15
u8 drones, gunners, hunters, builders; #define TP_POOL_SIZE 5
u8 bossHp;
u8 treasures; // pool index -> enemy type mapping
u8 gunnerPattern; static const u8 poolTypeMap[TP_POOL_SIZE] = {
bool dronesShoot; ENEMY_TYPE_TEST, // 0: Fairy
ENEMY_TYPE_DRONE, // 1: Drone
ENEMY_TYPE_GUNNER, // 2: Gunner
ENEMY_TYPE_HUNTER, // 3: Hunter
ENEMY_TYPE_BUILDER, // 4: Builder
}; };
// dr gn hn bl boss tre pat shoot // TP costs per pool index
const struct LevelDef levels[20] = { static const u8 typeCost[TP_POOL_SIZE] = { 5, 2, 4, 3, 3 };
// Phase 1: "Immediate danger" (L1-L4) static const u8 typeWeight[TP_POOL_SIZE] = { 2, 8, 4, 3, 3 };
{ 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1 static const u8 typeMaxCount[TP_POOL_SIZE] = { 3, 16, 6, 6, 2 };
{ 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2 static const u8 typeMinCount[TP_POOL_SIZE] = { 0, 2, 0, 0, 0 };
{ 12, 3, 0, 0, 0, 8, 1, TRUE }, // L3
{ 8, 0, 0, 0, 25, 8, 0, TRUE }, // L4 BOSS 1
// Phase 2: "You can't save everything" (L5-L8) // Boss HP per boss number (0-4)
{ 10, 2, 0, 1, 0, 8, 0, TRUE }, // L5 static const s16 bossHpTable[5] = { 24, 50, 75, 100, 125 };
{ 14, 3, 0, 1, 0, 8, 1, TRUE }, // L6
{ 10, 2, 0, 2, 0, 8, 2, TRUE }, // L7
{ 8, 0, 0, 1, 50, 8, 0, TRUE }, // L8 BOSS 2
// Phase 3: "Geometry matters" (L9-L12) // Returns bitmask of unlocked pool indices for a given level
{ 8, 3, 4, 0, 0, 8, 1, TRUE }, // L9 static u8 getUnlockedTypes(u8 lvl){
{ 10, 2, 4, 0, 0, 8, 2, TRUE }, // L10 u8 mask = 0;
{ 12, 3, 3, 0, 0, 8, 1, TRUE }, // L11 // Drone always unlocked
{ 0, 2, 2, 0, 75, 8, 2, TRUE }, // L12 BOSS 3 mask |= (1 << 1);
// Gunner from L2 (index 1)
if(lvl >= 1) mask |= (1 << 2);
// Fairy + Builder from L4 (index 3)
if(lvl >= 3){
mask |= (1 << 0);
mask |= (1 << 4);
}
// Hunter from L7 (index 6)
if(lvl >= 6) mask |= (1 << 3);
return mask;
}
// Phase 4: "Suffocation" (L13-L16) static u8 getTreasureCount(u8 lvl){
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L13 if(lvl == 0) return 4;
{ 10, 0, 6, 0, 0, 8, 0, TRUE }, // L14 if(lvl <= 2) return 6;
{ 12, 4, 2, 0, 0, 8, 1, TRUE }, // L15 return 8;
{ 0, 3, 0, 1, 100, 8, 2, TRUE }, // L16 BOSS 4 }
// Phase 5: "Arcade cruelty" (L17-L20) static void assignGunnerPatterns(u8 lvl){
{ 16, 0, 4, 0, 0, 8, 0, TRUE }, // L17 u8 pat;
{ 14, 4, 4, 2, 0, 8, 2, TRUE }, // L18 if(lvl < 3) pat = 0; // Cycle 1: radial burst
{ 6, 2, 2, 1, 50, 8, 2, TRUE }, // L19 MINI-BOSS else if(lvl < 6) pat = 1; // Cycle 2: aimed fan
{ 4, 2, 2, 1, 125, 8, 2, TRUE }, // L20 BOSS 5 FINAL else pat = 2; // Cycle 3+: mix
};
#define LEVEL_COUNT 20 for(s16 i = 0; i < ENEMY_COUNT; i++){
if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){
if(pat == 2)
enemies[i].ints[0] = random() % 2;
else
enemies[i].ints[0] = pat;
}
}
}
static void distributeEnemies(u8 type, u8 count){ static void distributeEnemies(u8 type, u8 count){
for(u8 i = 0; i < count; i++){ for(u8 i = 0; i < count; i++){
u8 zone = i % 4; u8 zone = i % SECTION_COUNT;
spawnEnemy(type, zone); spawnEnemy(type, zone);
} }
} }
// Generate enemy counts into the provided array (indexed by pool index)
static void generateLevel(u8 lvl, u8* counts){
for(u8 i = 0; i < TP_POOL_SIZE; i++) counts[i] = 0;
// L1 special case: 4 non-shooting drones
if(lvl == 0){
counts[1] = 4;
return;
}
// Compute budget
u16 budget = 8 + (lvl * 4) + (lvl * lvl / 5);
budget = (budget * (90 + (random() % 21))) / 100;
// Boss levels get 40% escort budget
if(isBossLevel(lvl)){
budget = (budget * 40) / 100;
if(budget < 4) budget = 4;
}
u8 unlocked = getUnlockedTypes(lvl);
u8 maxTotal = isBossLevel(lvl) ? ENEMY_COUNT - 1 : ENEMY_COUNT; // reserve 1 slot for boss
// Apply minimum guarantees
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if((unlocked & (1 << i)) && typeMinCount[i] > 0){
counts[i] = typeMinCount[i];
u16 cost = typeMinCount[i] * typeCost[i];
if(cost <= budget) budget -= cost;
else budget = 0;
}
}
// Shopping loop
u16 safety = 0;
u8 totalEnemies = 0;
for(u8 i = 0; i < TP_POOL_SIZE; i++) totalEnemies += counts[i];
while(budget >= 2 && totalEnemies < maxTotal && safety < 100){
safety++;
// Boss escort restriction: only drones + builders
u8 escortMask = isBossLevel(lvl) ? ((1 << 1) | (1 << 4)) : 0xFF;
// Build weighted pool of affordable, unlocked, non-maxed types
u16 totalWeight = 0;
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if(!(unlocked & (1 << i))) continue;
if(!(escortMask & (1 << i))) continue;
if(counts[i] >= typeMaxCount[i]) continue;
if(typeCost[i] > budget) continue;
totalWeight += typeWeight[i];
}
if(totalWeight == 0) break;
// Weighted random pick
u16 roll = random() % totalWeight;
u16 accum = 0;
u8 picked = 0xFF;
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if(!(unlocked & (1 << i))) continue;
if(!(escortMask & (1 << i))) continue;
if(counts[i] >= typeMaxCount[i]) continue;
if(typeCost[i] > budget) continue;
accum += typeWeight[i];
if(roll < accum){
picked = i;
break;
}
}
if(picked == 0xFF) break;
counts[picked]++;
budget -= typeCost[picked];
totalEnemies++;
}
}
void loadLevel(u8 lvl){ void loadLevel(u8 lvl){
if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1; if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1;
level = lvl; level = lvl;
const struct LevelDef* def = &levels[lvl]; grazeCount = 0;
distributeEnemies(ENEMY_TYPE_DRONE, def->drones); // Generate enemy composition
distributeEnemies(ENEMY_TYPE_GUNNER, def->gunners); u8 counts[TP_POOL_SIZE];
distributeEnemies(ENEMY_TYPE_HUNTER, def->hunters); generateLevel(lvl, counts);
distributeEnemies(ENEMY_TYPE_BUILDER, def->builders);
// set gunner pattern based on level def // Spawn enemies by type
for(s16 i = 0; i < ENEMY_COUNT; i++){ for(u8 i = 0; i < TP_POOL_SIZE; i++){
if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){ if(counts[i] > 0)
if(def->gunnerPattern == 2) distributeEnemies(poolTypeMap[i], counts[i]);
enemies[i].ints[0] = random() % 2;
else
enemies[i].ints[0] = def->gunnerPattern;
}
} }
if(def->bossHp > 0){ // Assign gunner patterns
pendingBossHp = def->bossHp; assignGunnerPatterns(lvl);
pendingBossNum = lvl / 4; // L3=0, L7=1, L11=2, L15=3, L18+=4
// Boss spawn
if(isBossLevel(lvl)){
pendingBossNum = lvl / 3;
if(pendingBossNum > 4) pendingBossNum = 4; if(pendingBossNum > 4) pendingBossNum = 4;
if(lvl == 18) pendingBossNum = 1; // L19 mini-boss reuses boss 2 pendingBossHp = bossHpTable[pendingBossNum];
spawnEnemy(ENEMY_TYPE_BOSS, 1); spawnEnemy(ENEMY_TYPE_BOSS, 1);
} }
// spawn treasures // Spawn treasures
u8 treasureToSpawn = def->treasures; u8 treasureToSpawn = getTreasureCount(lvl);
for(u8 zone = 0; zone < 4 && treasureToSpawn > 0; zone++){ for(u8 zone = 0; zone < SECTION_COUNT && treasureToSpawn > 0; zone++){
u8 perZone = treasureToSpawn >= 4 ? 2 : 1; u8 perZone = treasureToSpawn >= 4 ? 2 : 1;
for(u8 h = 0; h < perZone && treasureToSpawn > 0; h++){ for(u8 h = 0; h < perZone && treasureToSpawn > 0; h++){
spawnTreasure(zone); spawnTreasure(zone);
@ -178,6 +193,7 @@ void loadLevel(u8 lvl){
} }
} }
loadBgPalette(lvl % 3);
loadMap(); loadMap();
} }

107
src/starfield.h Normal file
View file

@ -0,0 +1,107 @@
#define STAR_TILE_I 290 // tile 290 = faded, 291 = full bright
#define STAR_COUNT 64
#define STAR_SPEED FIX16(0.7) // base max speed in tiles/frame (try 0.31.5)
#define STAR_FADE_FRAMES 5 // frames: tile 290 (faded)
#define STAR_MID_FRAMES 20 // frames: tile 291 (mid)
#define STAR_BRIGHT_FRAMES 40 // frames: tile 292 (bright); tile 293 (full) after this
#define STAR_SCREEN_W 40
#define STAR_SCREEN_H 28
#define STAR_CENTER_X 20
#define STAR_CENTER_Y 14
typedef struct {
fix16 x, y;
fix16 dx, dy;
u8 prevX, prevY;
u8 age; // frames since spawn; drives tile stage
u8 variant; // 0 = base tiles (290-293), 1 = alt color tiles (294-297)
} StarParticle;
static StarParticle stars[STAR_COUNT];
static u8 spawnCounter;
static u8 starVariant;
static void spawnStar(u8 i){
fix16 angleDeg = FIX16(random() % 360);
fix16 speed = F16_mul(STAR_SPEED, FIX16(0.25) + (fix16)((random() % 8) * FIX16(0.11)));
stars[i].x = FIX16(STAR_CENTER_X);
stars[i].y = FIX16(STAR_CENTER_Y);
stars[i].dx = F16_mul(F16_cos(angleDeg), speed);
stars[i].dy = F16_mul(F16_sin(angleDeg), speed);
stars[i].prevX = 255;
stars[i].prevY = 255;
stars[i].age = 0;
stars[i].variant = starVariant;
spawnCounter++;
}
// Advance a star forward by a random number of frames (no VDP writes).
// If it exits the screen during pre-advance, respawn it from center.
static void preadvanceStar(u8 i){
u8 frames = (u8)(random() % 90);
for(u8 f = 0; f < frames; f++){
stars[i].x += stars[i].dx;
stars[i].y += stars[i].dy;
if(stars[i].age < STAR_BRIGHT_FRAMES) stars[i].age++;
s16 tx = F16_toInt(stars[i].x);
s16 ty = F16_toInt(stars[i].y);
if(tx < 0 || tx >= STAR_SCREEN_W || ty < 0 || ty >= STAR_SCREEN_H){
spawnStar(i);
return;
}
}
}
void loadStarfield(u8 variant){
starVariant = variant;
VDP_loadTileSet(&starTiles, STAR_TILE_I, DMA);
// Reset BG_B scroll so stars appear at screen positions 0-39, 0-27
VDP_setVerticalScroll(BG_B, 0);
s16 zeroScroll[28] = {0};
VDP_setHorizontalScrollTile(BG_B, 0, zeroScroll, 28, DMA);
spawnCounter = 0;
for(u8 i = 0; i < STAR_COUNT; i++){
spawnStar(i);
preadvanceStar(i);
}
}
void clearStarfield(){
// Erase any star tiles still on BG_B before the next level loads
for(u8 i = 0; i < STAR_COUNT; i++){
if(stars[i].prevX != 255){
VDP_setTileMapXY(BG_B, TILE_ATTR(0, 0, 0, 0), stars[i].prevX, stars[i].prevY);
stars[i].prevX = 255;
stars[i].prevY = 255;
}
}
}
void updateStarfield(){
for(u8 i = 0; i < STAR_COUNT; i++){
if(stars[i].prevX != 255)
VDP_setTileMapXY(BG_B, TILE_ATTR(0, 0, 0, 0), stars[i].prevX, stars[i].prevY);
stars[i].x += stars[i].dx;
stars[i].y += stars[i].dy;
if(stars[i].age < STAR_BRIGHT_FRAMES) stars[i].age++;
s16 tx = F16_toInt(stars[i].x);
s16 ty = F16_toInt(stars[i].y);
if(tx < 0 || tx >= STAR_SCREEN_W || ty < 0 || ty >= STAR_SCREEN_H){
spawnStar(i);
continue;
}
u16 base = STAR_TILE_I + stars[i].variant * 4;
u16 tileIdx = (stars[i].age < STAR_FADE_FRAMES) ? base :
(stars[i].age < STAR_MID_FRAMES) ? (base + 1) :
(stars[i].age < STAR_BRIGHT_FRAMES) ? (base + 2) : (base + 3);
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, tileIdx), (u16)tx, (u16)ty);
stars[i].prevX = (u8)tx;
stars[i].prevY = (u8)ty;
}
}

View file

@ -5,40 +5,75 @@
s16 startClock; s16 startClock;
static void updateTransition(s16 startTime, bool last){ static void drawStartSplash(){
if(startClock >= startTime && startClock < startTime + TRANS_TIME){ VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 12, 6, 0, DMA);
switch(startClock - startTime){
case 0: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break;
case 5: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break;
case 10: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break;
case 15: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H); break;
case 20: VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H); break;
case TRANS_TIME - 20: if(!last){
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H);
} break;
case TRANS_TIME - 15: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break;
case TRANS_TIME - 10: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break;
case TRANS_TIME - 5: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break;
}
}
} }
static void drawStartSplash(){ s16 startScroll;
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I), 0, 0, START_W, START_H);
VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 13, 7, 0, DMA);
}
static void drawStartBg(){ static void drawStartBg(){
VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H); VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H);
VDP_drawImageEx(BG_B, &startBigBg, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16), 0, 0, 0, DMA); VDP_drawImageEx(BG_B, &startBg1, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16), 0, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg2, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196), 14, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg3, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196), 14 + 14, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg4, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196 + 168), 0, 14, 0, DMA);
VDP_drawImageEx(BG_B, &startBg5, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196 + 168 + 196), 14, 14, 0, DMA);
VDP_drawImageEx(BG_B, &startBg6, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196 + 168 + 196 + 196), 14 + 14, 14, 0, DMA);
startScroll = -160;
VDP_setVerticalScroll(BG_A, startScroll);
}
// State variables
static s16 startState; // 0 = main menu, 1 = music room
static s16 menuCursor; // 0 = START GAME, 1 = MUSIC ROOM
static s16 musicCursor; // 0-2, highlighted track
static s16 musicPlaying; // -1 = none, 0-2 = playing track index
static s16 musicDelay; // frames to delay before starting new track
static bool playBgmStart; // TRUE to play bgmStart after delay (from exiting music room)
static bool attractPending; // TRUE once pre-attract fade-out starts (blocks input)
static const char* trackNames[3] = {
"Title",
"Stage",
"Boss "
};
static const u8* musicTrackList[3];
#define MENU_X 14
#define MENU_Y 16
static void drawStartMenuCursors(){
VDP_drawText(menuCursor == 0 ? "%Start Game" : " Start Game", MENU_X, MENU_Y);
VDP_drawText(menuCursor == 1 ? "%Music Room" : " Music Room", MENU_X, MENU_Y + 2);
}
static bool startMenuTopDrawn, startMenuMidDrawn, startMenuBottomDrawn;
static void drawStartMenuTop(){
VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, 1111), 7, 7, 0, DMA);
VDP_drawText(" DRAGON-EATING DESCENT II", 7, 12);
startMenuTopDrawn = TRUE;
}
static void drawStartMenuMid(){
drawStartMenuCursors();
startMenuMidDrawn = TRUE;
}
static void drawStartMenuBottom(){
char hiScoreStr[SCORE_LENGTH];
uintToStr(highScore, hiScoreStr, 1);
VDP_drawText("High", 2, 25);
VDP_drawText(hiScoreStr, 2 + 5, 25);
VDP_drawText("2026 Peace Research", 38 - 19, 25);
startMenuBottomDrawn = TRUE;
} }
static void drawStartMenu(){ static void drawStartMenu(){
// VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 16 + 256), 26, 2, 0, DMA); drawStartMenuTop();
VDP_drawText(" PRESS ANY", 19, 18); drawStartMenuMid();
VDP_drawText(" BUTTON", 19, 19); drawStartMenuBottom();
VDP_drawText(" T. BODDY", 19, 24);
VDP_drawText(" 02.2026", 19, 25);
} }
static void loadGameFromStart(){ static void loadGameFromStart(){
@ -49,12 +84,122 @@ static void loadGameFromStart(){
loadGame(); loadGame();
} }
s16 startTime; static void loadAttractFromStart(){
static void updateStartMenu(){ isAttract = TRUE;
if(startTime == 0 && (ctrl.start || ctrl.a || ctrl.b || ctrl.c)){ attractClock = ATTRACT_DURATION;
XGM2_stop(); loadGameFromStart();
startTime = 30; }
#define MUSIC_NP_Y 25
#define MUSIC_CTRL_Y 17
static void drawMusicRoomCursors(){
for(s16 i = 0; i < 3; i++){
char line[16];
line[0] = (musicCursor == i) ? '%' : ' ';
line[1] = ' ';
const char* name = trackNames[i];
for(s16 j = 0; j < 11; j++) line[1 + j] = name[j];
line[13] = '\0';
VDP_drawText(line, 16, 13 + i);
} }
}
static void drawMusicRoomNowPlaying(){
if(musicPlaying >= 0){
char line[32];
const char* prefix = " Now Playing: ";
for(s16 i = 0; i < 15; i++) line[i] = prefix[i];
const char* name = trackNames[musicPlaying];
for(s16 i = 0; i < 11; i++) line[15 + i] = name[i];
line[26] = '\0';
VDP_drawText(line, 9, MUSIC_NP_Y);
} else {
VDP_drawText(" ", 9, MUSIC_NP_Y);
}
}
static void drawMusicRoom(){
VDP_drawImageEx(BG_A, &musicroom, TILE_ATTR_FULL(PAL0, 0, 0, 0, 1111), 15, 8, 0, DMA);
drawMusicRoomCursors();
VDP_drawText("[ Play/Stop ", 14, MUSIC_CTRL_Y);
VDP_drawText("] Back ", 14, MUSIC_CTRL_Y + 1);
drawMusicRoomNowPlaying();
}
static void enterMusicRoom(){
XGM2_stop();
startState = 1;
musicCursor = 0;
musicPlaying = -1;
VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H);
drawMusicRoom();
}
static void exitMusicRoom(){
XGM2_stop();
musicPlaying = -1;
musicDelay = 1;
playBgmStart = TRUE;
startState = 0;
startClock = TRANS_TIME + 41;
VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H);
drawStartMenu();
}
s16 startTime;
static bool prevUp, prevDown, prevA, prevB, prevC, prevStart;
static void updateStartMainMenu(){
if(attractPending) return;
// handle music delay
if(musicDelay > 0){
musicDelay--;
if(musicDelay == 0 && playBgmStart){
#if MUSIC_VOLUME > 0
XGM2_play(bgmStart);
#endif
playBgmStart = FALSE;
}
}
// Up/down cursor movement
bool curUp = ctrl.up;
bool curDown = ctrl.down;
if(curUp && !prevUp){
menuCursor = (menuCursor == 0) ? 1 : 0;
drawStartMenuCursors();
sfxMenuSelect();
startClock = TRANS_TIME + 21;
}
if(curDown && !prevDown){
menuCursor = (menuCursor == 0) ? 1 : 0;
drawStartMenuCursors();
sfxMenuSelect();
startClock = TRANS_TIME + 21;
}
prevUp = curUp;
prevDown = curDown;
// Confirm selection
bool curA = ctrl.a;
bool curB = ctrl.b;
bool curSt = ctrl.start;
if((curA && !prevA) || (curSt && !prevStart)){
if(startTime == 0){
if(menuCursor == 0){
startTime = 30;
sfxStartGame();
PAL_fadeOut(0, 47, 20, TRUE);
} else {
enterMusicRoom();
}
}
}
prevA = curA;
prevB = curB;
prevStart = curSt;
if(startTime > 0){ if(startTime > 0){
startTime--; startTime--;
if(startTime <= 0){ if(startTime <= 0){
@ -63,20 +208,110 @@ static void updateStartMenu(){
} }
} }
static void updateMusicRoom(){
// handle music delay
if(musicDelay > 0){
musicDelay--;
if(musicDelay == 0 && musicPlaying >= 0){
#if MUSIC_VOLUME > 0
XGM2_play(musicTrackList[musicPlaying]);
#endif
}
}
bool curUp = ctrl.up;
bool curDown = ctrl.down;
bool curA = ctrl.a;
bool curB = ctrl.b;
bool curSt = ctrl.start;
if(curUp && !prevUp){
musicCursor = (musicCursor == 0) ? 2 : musicCursor - 1;
drawMusicRoomCursors();
sfxMenuSelect();
}
if(curDown && !prevDown){
musicCursor = (musicCursor == 2) ? 0 : musicCursor + 1;
drawMusicRoomCursors();
sfxMenuSelect();
}
if((curA && !prevA) || (curSt && !prevStart)){
if(musicPlaying == musicCursor){
XGM2_stop();
musicPlaying = -1;
} else {
XGM2_stop();
musicDelay = 1;
musicPlaying = musicCursor;
}
drawMusicRoomNowPlaying();
}
if(curB && !prevB){
exitMusicRoom();
}
prevUp = curUp;
prevDown = curDown;
prevA = curA;
prevB = curB;
prevStart = curSt;
}
void loadStart(){ void loadStart(){
VDP_loadTileSet(startFade1.tileset, START_I, DMA); static const u16 palBlack[64] = {0};
VDP_loadTileSet(startFade2.tileset, START_I + 1, DMA); PAL_setColors(0, palBlack, 64, CPU);
VDP_loadTileSet(startFade3.tileset, START_I + 2, DMA); getHighScore();
VDP_loadTileSet(startFade4.tileset, START_I + 3, DMA); musicTrackList[0] = bgmStart;
musicTrackList[1] = stageMusic;
musicTrackList[2] = bossMusic;
startState = 0;
menuCursor = 0;
musicCursor = 0;
musicPlaying = -1;
prevUp = FALSE; prevDown = FALSE; prevA = FALSE;
prevB = FALSE; prevC = FALSE; prevStart = FALSE;
attractPending = FALSE;
startMenuTopDrawn = FALSE;
startMenuMidDrawn = FALSE;
startMenuBottomDrawn = FALSE;
drawStartSplash(); drawStartSplash();
XGM2_play(bgmStart);
} }
void updateStart(){ void updateStart(){
updateTransition(0, FALSE); if(startScroll < 0 && startClock >= TRANS_TIME){
updateTransition(TRANS_TIME, TRUE); startScroll += 8;
if(startClock == TRANS_TIME) drawStartBg(); // if(startScroll > 0) startScroll = 0;
else if(startClock == TRANS_TIME + 40) drawStartMenu(); VDP_setVerticalScroll(BG_A, startScroll);
else if(startClock > TRANS_TIME + 40) updateStartMenu(); }
if(startClock == 0)
PAL_fadeIn(0, 15, font.palette->data, 20, TRUE);
if(startClock == TRANS_TIME - 20)
PAL_fadeOutAll(20, TRUE);
if(startClock == TRANS_TIME){
drawStartBg();
#if MUSIC_VOLUME > 0
XGM2_play(bgmStart);
#endif
u16 menuPal[32];
memcpy(menuPal, font.palette->data, 16 * sizeof(u16));
memcpy(menuPal + 16, shadow.palette->data, 16 * sizeof(u16));
PAL_fadeIn(0, 31, menuPal, 20, TRUE);
drawStartMenuTop();
}
// stagger menu/bottom text to avoid plane-wrap visibility
if(startClock > TRANS_TIME){
if(!startMenuMidDrawn && startScroll >= -104) drawStartMenuMid();
if(!startMenuBottomDrawn && startScroll >= -32) drawStartMenuBottom();
}
if(startClock > TRANS_TIME + 20){
if(startState == 0) updateStartMainMenu();
else updateMusicRoom();
}
// Attract mode only from main menu
if(startState == 0 && !attractPending && startClock == TRANS_TIME + 20 + ATTRACT_LIMIT - 20 && startTime == 0){
attractPending = TRUE;
PAL_fadeOut(0, 31, 20, TRUE);
}
if(startState == 0 && startClock >= TRANS_TIME + 20 + ATTRACT_LIMIT && startTime == 0) loadAttractFromStart();
if(startClock < CLOCK_LIMIT) startClock++; if(startClock < CLOCK_LIMIT) startClock++;
} }

View file

@ -19,12 +19,13 @@ void spawnTreasure(u8 zone){
treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1); treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
treasures[i].image = SPR_addSprite(&treasureSprite, treasures[i].image = SPR_addSprite(&treasureSprite,
getScreenX(treasures[i].pos.x, player.camera) - TREASURE_OFF, fix32ToInt(treasures[i].pos.y) - TREASURE_OFF, getScreenX(treasures[i].pos.x, player.camera) - TREASURE_OFF, F32_toInt(treasures[i].pos.y) - TREASURE_OFF,
TILE_ATTR(PAL0, 0, 0, 0)); TILE_ATTR(PAL0, 0, 0, 0));
if(!treasures[i].image){ if(!treasures[i].image){
treasures[i].active = FALSE; treasures[i].active = FALSE;
return; return;
} }
SPR_setDepth(treasures[i].image, 2);
SPR_setVisibility(treasures[i].image, HIDDEN); SPR_setVisibility(treasures[i].image, HIDDEN);
treasures[i].type = random() % 4; treasures[i].type = random() % 4;
SPR_setAnim(treasures[i].image, treasures[i].type); SPR_setAnim(treasures[i].image, treasures[i].type);
@ -34,8 +35,13 @@ static void updateTreasure(u8 i){
switch(treasures[i].state){ switch(treasures[i].state){
case TREASURE_WALKING: case TREASURE_WALKING:
// Y bounce: bob 4px around ground level // Y bounce: bob 4px around ground level
if(treasures[i].pos.y >= GAME_H_F - FIX32(20) || treasures[i].pos.y <= GAME_H_F - FIX32(28)) if(treasures[i].pos.y >= GAME_H_F - FIX32(16)){
treasures[i].vel.y *= -1; if(treasures[i].vel.y > 0) treasures[i].vel.y = -treasures[i].vel.y;
treasures[i].pos.y = GAME_H_F - FIX32(16);
} else if(treasures[i].pos.y <= GAME_H_F - FIX32(32)){
if(treasures[i].vel.y < 0) treasures[i].vel.y = -treasures[i].vel.y;
treasures[i].pos.y = GAME_H_F - FIX32(32);
}
// X wrap // X wrap
if(treasures[i].pos.x >= GAME_WRAP) if(treasures[i].pos.x >= GAME_WRAP)
@ -43,8 +49,8 @@ static void updateTreasure(u8 i){
if(treasures[i].pos.x < 0) if(treasures[i].pos.x < 0)
treasures[i].pos.x += GAME_WRAP; treasures[i].pos.x += GAME_WRAP;
treasures[i].pos.x += treasures[i].vel.x; treasures[i].pos.x += treasures[i].vel.x - (player.vel.x >> 2);
treasures[i].pos.y += treasures[i].vel.y; treasures[i].pos.y += treasures[i].vel.y - (playerScrollVelY >> 3);
break; break;
case TREASURE_CARRIED: case TREASURE_CARRIED:
@ -109,8 +115,20 @@ static void updateTreasure(u8 i){
if(treasures[i].state != TREASURE_CARRIED && treasures[i].state != TREASURE_COLLECTED){ if(treasures[i].state != TREASURE_CARRIED && treasures[i].state != TREASURE_COLLECTED){
fix32 dy = treasures[i].pos.y - player.pos.y; fix32 dy = treasures[i].pos.y - player.pos.y;
if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){ if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){
score += (treasures[i].state == TREASURE_FALLING) ? 2000 : 1000; score += (treasures[i].state == TREASURE_FALLING) ? 4096 : 1024;
sfxPickup(); // check if this is the last treasure (all others inactive or collected)
bool willBeLast = TRUE;
for(s16 j = 0; j < TREASURE_COUNT; j++){
if(j != i && treasures[j].active && treasures[j].state != TREASURE_COLLECTED){
willBeLast = FALSE;
break;
}
}
if(willBeLast){
sfxCollectAllTreasures();
} else {
sfxCollectTreasure();
}
treasureCollectedType = treasures[i].type; treasureCollectedType = treasures[i].type;
treasureCollectedClock = 120; treasureCollectedClock = 120;
// only add to trail if this type isn't already collected // only add to trail if this type isn't already collected
@ -133,12 +151,12 @@ static void updateTreasure(u8 i){
} }
s16 sx = getScreenX(treasures[i].pos.x, player.camera); s16 sx = getScreenX(treasures[i].pos.x, player.camera);
s16 sy = fix32ToInt(treasures[i].pos.y); s16 sy = F32_toInt(treasures[i].pos.y);
bool visible = (treasures[i].state == TREASURE_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); bool visible = (treasures[i].state == TREASURE_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
if(visible && treasures[i].state == TREASURE_COLLECTED){ if(visible && treasures[i].state == TREASURE_COLLECTED){
if(player.respawnClock > 0) if(player.respawnClock > 0)
visible = FALSE; visible = FALSE;
else if(player.recoveringClock > 0) else if(player.recoveringClock > 0 && player.recoverFlash)
visible = (player.recoveringClock % 20 > 10); visible = (player.recoveringClock % 20 > 10);
} }
SPR_setVisibility(treasures[i].image, visible ? VISIBLE : HIDDEN); SPR_setVisibility(treasures[i].image, visible ? VISIBLE : HIDDEN);