diff --git a/build.sh b/build.sh index 41b0d7a..faabf93 100755 --- a/build.sh +++ b/build.sh @@ -2,5 +2,6 @@ rm -rf res/resources.o res/resources.h out/* # make # ./blastem/blastem out.bin #dgen out.bin -docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00 -/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin \ No newline at end of file +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/ares.app/Contents/MacOS/ares out/rom.bin --system "Mega Drive" \ No newline at end of file diff --git a/compile.sh b/compile.sh index 86704ed..fada6ac 100755 --- a/compile.sh +++ b/compile.sh @@ -1,2 +1,2 @@ rm -rf res/resources.o res/resources.h out/* -docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00 \ No newline at end of file +docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.11 \ No newline at end of file diff --git a/default.profraw b/default.profraw new file mode 100644 index 0000000..c1a70fd Binary files /dev/null and b/default.profraw differ diff --git a/res/bullets.png b/res/bullets.png index b234322..de02bb2 100644 Binary files a/res/bullets.png and b/res/bullets.png differ diff --git a/res/door.png b/res/door.png new file mode 100644 index 0000000..975adf7 Binary files /dev/null and b/res/door.png differ diff --git a/res/enemies/boss1.png b/res/enemies/boss1.png new file mode 100644 index 0000000..ba1f623 Binary files /dev/null and b/res/enemies/boss1.png differ diff --git a/res/enemies/boss2.png b/res/enemies/boss2.png new file mode 100644 index 0000000..2f710e6 Binary files /dev/null and b/res/enemies/boss2.png differ diff --git a/res/enemies/boss3.png b/res/enemies/boss3.png new file mode 100644 index 0000000..587fa35 Binary files /dev/null and b/res/enemies/boss3.png differ diff --git a/res/enemies/boss4.png b/res/enemies/boss4.png new file mode 100644 index 0000000..fd3e0a5 Binary files /dev/null and b/res/enemies/boss4.png differ diff --git a/res/eyebig.png b/res/eyebig.png new file mode 100644 index 0000000..ac8ed41 Binary files /dev/null and b/res/eyebig.png differ diff --git a/res/font.png b/res/font.png index fe18dd4..5a32501 100644 Binary files a/res/font.png and b/res/font.png differ diff --git a/res/fontbig.png b/res/fontbig.png index d394a91..a9f4c3f 100644 Binary files a/res/fontbig.png and b/res/fontbig.png differ diff --git a/res/fontbigger.png b/res/fontbigger.png new file mode 100644 index 0000000..bb82b40 Binary files /dev/null and b/res/fontbigger.png differ diff --git a/res/ground.png b/res/ground.png index c57aed4..5eb40e5 100644 Binary files a/res/ground.png and b/res/ground.png differ diff --git a/res/life.png b/res/life.png index 6a7c981..d66ff40 100644 Binary files a/res/life.png and b/res/life.png differ diff --git a/res/life2.png b/res/life2.png index 2b01299..dccd704 100644 Binary files a/res/life2.png and b/res/life2.png differ diff --git a/res/mapindicator.png b/res/mapindicator.png index 5b9d55d..4b487d6 100644 Binary files a/res/mapindicator.png and b/res/mapindicator.png differ diff --git a/res/musicroom.png b/res/musicroom.png new file mode 100644 index 0000000..c43161f Binary files /dev/null and b/res/musicroom.png differ diff --git a/res/resources.res b/res/resources.res index a8b3413..1cb9f33 100644 --- a/res/resources.res +++ b/res/resources.res @@ -1,19 +1,22 @@ IMAGE font "font.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 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" IMAGE sky "sky.png" NONE NONE IMAGE skyTop "skytop.png" NONE NONE IMAGE skyRed "skyred.png" NONE NONE IMAGE ground "ground.png" NONE NONE +IMAGE door "door.png" NONE NONE 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 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 IMAGE mapIndicator "mapindicator.png" NONE NONE +TILESET starTiles "stars.png" NONE IMAGE imageFontBig "fontbig.png" NONE NONE +IMAGE imageFontBigger "fontbigger.png" NONE NONE IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE IMAGE imageChromeLife "life.png" NONE NONE IMAGE imageChromeLife2 "life2.png" NONE NONE -XGM2 stageMusic "level.vgm" \ No newline at end of file +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 diff --git a/res/sfx/beatgame.wav b/res/sfx/beatgame.wav new file mode 100644 index 0000000..483d03c Binary files /dev/null and b/res/sfx/beatgame.wav differ diff --git a/res/sfx/beatlevel.wav b/res/sfx/beatlevel.wav new file mode 100644 index 0000000..0aa3a36 Binary files /dev/null and b/res/sfx/beatlevel.wav differ diff --git a/res/sfx/bullet1.wav b/res/sfx/bullet1.wav new file mode 100644 index 0000000..3678986 Binary files /dev/null and b/res/sfx/bullet1.wav differ diff --git a/res/sfx/bullet2.wav b/res/sfx/bullet2.wav new file mode 100644 index 0000000..42d4da5 Binary files /dev/null and b/res/sfx/bullet2.wav differ diff --git a/res/sfx/bullet3.wav b/res/sfx/bullet3.wav new file mode 100644 index 0000000..940aa99 Binary files /dev/null and b/res/sfx/bullet3.wav differ diff --git a/res/sfx/explosion1.wav b/res/sfx/explosion1.wav new file mode 100644 index 0000000..c1fac9e Binary files /dev/null and b/res/sfx/explosion1.wav differ diff --git a/res/sfx/explosion2.wav b/res/sfx/explosion2.wav new file mode 100644 index 0000000..60659bf Binary files /dev/null and b/res/sfx/explosion2.wav differ diff --git a/res/sfx/gameover.wav b/res/sfx/gameover.wav new file mode 100644 index 0000000..7e7a455 Binary files /dev/null and b/res/sfx/gameover.wav differ diff --git a/res/sfx/menuchoose.wav b/res/sfx/menuchoose.wav new file mode 100644 index 0000000..e8a5def Binary files /dev/null and b/res/sfx/menuchoose.wav differ diff --git a/res/sfx/menuselect.wav b/res/sfx/menuselect.wav new file mode 100644 index 0000000..6d2e5f1 Binary files /dev/null and b/res/sfx/menuselect.wav differ diff --git a/res/sfx/playerhit.wav b/res/sfx/playerhit.wav new file mode 100644 index 0000000..def025b Binary files /dev/null and b/res/sfx/playerhit.wav differ diff --git a/res/sfx/playershot.wav b/res/sfx/playershot.wav new file mode 100644 index 0000000..e518440 Binary files /dev/null and b/res/sfx/playershot.wav differ diff --git a/res/sfx/startgame.wav b/res/sfx/startgame.wav new file mode 100644 index 0000000..d531271 Binary files /dev/null and b/res/sfx/startgame.wav differ diff --git a/res/sky.png b/res/sky.png index 0e23ae5..279548b 100644 Binary files a/res/sky.png and b/res/sky.png differ diff --git a/res/skyred.png b/res/skyred.png index de2f688..f057ece 100644 Binary files a/res/skyred.png and b/res/skyred.png differ diff --git a/res/skytop.png b/res/skytop.png index a25d59e..f67fc25 100644 Binary files a/res/skytop.png and b/res/skytop.png differ diff --git a/res/stars.png b/res/stars.png new file mode 100644 index 0000000..649562a Binary files /dev/null and b/res/stars.png differ diff --git a/res/start/bg1.png b/res/start/bg1.png new file mode 100644 index 0000000..55ef3b5 Binary files /dev/null and b/res/start/bg1.png differ diff --git a/res/start/bg2.png b/res/start/bg2.png new file mode 100644 index 0000000..d03e6ca Binary files /dev/null and b/res/start/bg2.png differ diff --git a/res/start/bg3.png b/res/start/bg3.png new file mode 100644 index 0000000..7f041cc Binary files /dev/null and b/res/start/bg3.png differ diff --git a/res/start/bg4.png b/res/start/bg4.png new file mode 100644 index 0000000..e793937 Binary files /dev/null and b/res/start/bg4.png differ diff --git a/res/start/bg5.png b/res/start/bg5.png new file mode 100644 index 0000000..934b952 Binary files /dev/null and b/res/start/bg5.png differ diff --git a/res/start/bg6.png b/res/start/bg6.png new file mode 100644 index 0000000..28fd5d8 Binary files /dev/null and b/res/start/bg6.png differ diff --git a/res/start/logo.png b/res/start/logo.png index 05a1ae2..3ff08e2 100644 Binary files a/res/start/logo.png and b/res/start/logo.png differ diff --git a/res/start/splash1.png b/res/start/splash1.png index 57d3765..856532a 100644 Binary files a/res/start/splash1.png and b/res/start/splash1.png differ diff --git a/run.sh b/run.sh index f544178..a96a093 100755 --- a/run.sh +++ b/run.sh @@ -1 +1,2 @@ -/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin \ No newline at end of file +#/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin +/Applications/ares.app/Contents/MacOS/ares out/rom.bin --system "Mega Drive" \ No newline at end of file diff --git a/src/background.h b/src/background.h index ab3a23a..d5b4db5 100644 --- a/src/background.h +++ b/src/background.h @@ -1,11 +1,13 @@ #define BG_I 8 -// zone-unique block: 64x64px ground block in sky area, only visible in zone 0 -// world X=256 = tile col 32, placed in sky row block 1 (tile row 8) -#define ZONE_BLOCK_WORLD_X 256 -#define ZONE_BLOCK_COL ((ZONE_BLOCK_WORLD_X / 8) % 128) +// doors: one per zone, placed in sky area at tile row 16 +// base X per zone chosen so tile cols never overlap between zone pairs: +// zone 0 → cols 1-31, zone 2 → cols 33-63 +// zone 1 → cols 65-95, zone 3 → cols 97-127 #define ZONE_BLOCK_ROW 16 -bool zoneBlockVisible; +#define DOOR_COUNT SECTION_COUNT +fix32 doorWorldX[DOOR_COUNT]; +bool doorVisible[DOOR_COUNT]; fix32 prevCamera; #define PARALLAX_COUNT 8 fix32 parallaxAccum[PARALLAX_COUNT]; @@ -15,6 +17,7 @@ static const fix32 parallaxMul[PARALLAX_COUNT] = { s16 bgScroll[28]; u8 bgOff; +u16 bgPal[16]; void loadBackground(){ VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE); @@ -23,6 +26,7 @@ void loadBackground(){ VDP_loadTileSet(sky.tileset, BG_I + 64, DMA); VDP_loadTileSet(ground.tileset, BG_I + 128, 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 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 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) - // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); - zoneBlockVisible = TRUE; + // place one door per zone at a random position within each zone's unique col range + for(u8 d = 0; d < DOOR_COUNT; d++){ + 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; 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 - 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++) - bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i])); + bgScroll[27 - i] = (initScroll - F32_toInt(parallaxAccum[i])); VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); } void updateBackground(){ - s16 scrollVal = fix32ToInt(-player.camera); + s16 scrollVal = F32_toInt(-player.camera); // accumulate parallax from camera delta (not absolute position) // this avoids discontinuities at world wrap boundaries @@ -75,7 +85,7 @@ void updateBackground(){ // update accumulators once, reuse for top and bottom 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); else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024); } @@ -83,18 +93,51 @@ void updateBackground(){ for(u8 i = 0; i < 20; i++) bgScroll[i] = scrollVal; 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); - // show ground block only when zone 0 copy of these columns is on screen - fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160)); - bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212)); - // if(shouldShow && !zoneBlockVisible){ - // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); - // zoneBlockVisible = TRUE; - // } else if(!shouldShow && zoneBlockVisible){ - // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); - // zoneBlockVisible = FALSE; - // } + // show/hide each door based on proximity to camera center + for(u8 d = 0; d < DOOR_COUNT; d++){ + fix32 dx = getWrappedDelta(doorWorldX[d] + FIX32(32), player.camera + FIX32(160)); + bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212)); + u8 col = (u8)(F32_toInt(doorWorldX[d]) / 8 % 128); + if(shouldShow && !doorVisible[d]){ + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 256), col, ZONE_BLOCK_ROW, 8, 8); + doorVisible[d] = TRUE; + } 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); } diff --git a/src/boot/sega.s b/src/boot/sega.s index 6dcde0b..097c35a 100644 --- a/src/boot/sega.s +++ b/src/boot/sega.s @@ -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: movem.l %d0-%d1/%a0-%a1,-(%sp) move.l intCB, %a0 diff --git a/src/bullets.h b/src/bullets.h index 7a813c9..1e1a831 100644 --- a/src/bullets.h +++ b/src/bullets.h @@ -1,68 +1,39 @@ #define BULLET_OFF 8 #define P_BULLET_OFF 16 -static void doBulletRotation(u8 i){ - if(bullets[i].anim >= FIRST_ROTATING_BULLET && !bullets[i].player){ +void doBulletRotation(u8 i){ + if(bullets[i].anim >= FIRST_ROTATING_BULLET){ bullets[i].vFlip = FALSE; bullets[i].hFlip = FALSE; - if(bullets[i].angle < 0) bullets[i].angle += 1024; - else if(bullets[i].angle >= 1024) bullets[i].angle -= 1024; + fix16 a = F16_normalizeAngle(bullets[i].angle); + 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 - 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; } - + bullets[i].frame = frame; SPR_setFrame(bullets[i].image, bullets[i].frame); SPR_setHFlip(bullets[i].image, bullets[i].hFlip); SPR_setVFlip(bullets[i].image, bullets[i].vFlip); } } -void spawnBullet(struct bulletSpawner spawner, void(*updater)){ - if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return; - // Don't spawn if offscreen +bool spawnBullet(struct bulletSpawner spawner, void(*updater)){ + if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return FALSE; + // Don't spawn if offscreen -- enemy bullets use tighter visible-screen limit 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)); - if(offScreenX || offScreenY) return; + if(offScreenX || offScreenY) return FALSE; // Find available slot, return if none s16 i = -1; @@ -74,32 +45,37 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){ break; } } - if(i == -1) return; + if(i == -1) return FALSE; + spawner.angle = F16_normalizeAngle(spawner.angle); bullets[i].active = TRUE; bullets[i].pos.x = spawner.x; bullets[i].pos.y = spawner.y; bullets[i].angle = spawner.angle; + bullets[i].speed = spawner.speed; bullets[i].player = spawner.player; bullets[i].clock = 0; if(spawner.vel.x || spawner.vel.y){ bullets[i].vel.x = spawner.vel.x; bullets[i].vel.y = spawner.vel.y; } else { - bullets[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(spawner.angle)), spawner.speed); - bullets[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(spawner.angle)), spawner.speed); + bullets[i].vel.x = F32_mul(F32_cos(spawner.angle), spawner.speed); + bullets[i].vel.y = F32_mul(F32_sin(spawner.angle), spawner.speed); } bullets[i].updater = updater; 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, - 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){ bullets[i].active = FALSE; - return; + return FALSE; } bullets[i].anim = spawner.anim; @@ -108,6 +84,12 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){ SPR_setFrame(bullets[i].image, spawner.frame); SPR_setDepth(bullets[i].image, spawner.player ? 7 : (spawner.top ? 3 : 4)); 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) @@ -119,9 +101,9 @@ static void collideWithEnemy(u8 i){ fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y; if(deltaY >= -BULLET_CHECK && deltaY <= 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){ - score += (enemies[j].ints[3] >= 0) ? 200 : 100; + score += (enemies[j].ints[3] >= 0) ? 512 : 256; killBullet(i, TRUE); killEnemy(j); sfxExplosion(); @@ -137,27 +119,54 @@ static void collideWithPlayer(u8 i){ fix32 deltaY = bullets[i].pos.y - player.pos.y; s32 dist = getApproximatedDistance( - fix32ToInt(deltaX), - fix32ToInt(deltaY)); + F32_toInt(deltaX), + F32_toInt(deltaY)); if(dist <= 4){ - // convert enemy bullet to player bullet explosion in-place - SPR_setDefinition(bullets[i].image, &pBulletSprite); - bullets[i].player = TRUE; - bullets[i].pos.x = player.pos.x; - bullets[i].pos.y = player.pos.y; - killBullet(i, TRUE); - sfxExplosion(); - player.lives--; - if(player.lives == 0){ - gameOver = TRUE; - XGM2_stop(); - } else { - player.respawnClock = 120; - SPR_setVisibility(player.image, HIDDEN); - killBullets = TRUE; - hitMessageClock = 120; - hitMessageBullet = TRUE; + // kill enemy bullet, then spawn a fresh player bullet explosion + killBullet(i, FALSE); + s16 expSlot = -1; + for(s16 j = 0; j < BULLET_COUNT; j++) if(!bullets[j].active){ expSlot = j; break; } + if(expSlot >= 0){ + bullets[expSlot].active = TRUE; + bullets[expSlot].player = TRUE; + bullets[expSlot].explosion = TRUE; + bullets[expSlot].pos.x = player.pos.x; + bullets[expSlot].pos.y = player.pos.y; + bullets[expSlot].vel.x = 0; + bullets[expSlot].vel.y = 0; + bullets[expSlot].clock = 0; + bullets[expSlot].frame = 0; + bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0)); + if(bullets[expSlot].image){ + 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); } s16 sx = getScreenX(bullets[i].pos.x, player.camera); - s16 sy = fix32ToInt(bullets[i].pos.y); - u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF; + s16 sy = F32_toInt(bullets[i].pos.y); + u8 off = BULLET_OFF; SPR_setPosition(bullets[i].image, sx - off, sy - off); } @@ -182,8 +191,8 @@ static void updateBullet(u8 i){ updateBulletExplosion(i); return; } - bullets[i].pos.x += bullets[i].vel.x; - bullets[i].pos.y += bullets[i].vel.y; + bullets[i].pos.x += bullets[i].vel.x - (player.vel.x >> 3); + bullets[i].pos.y += bullets[i].vel.y - (playerScrollVelY >> 3); if(bullets[i].pos.x >= GAME_WRAP){ bullets[i].pos.x -= GAME_WRAP; @@ -203,7 +212,7 @@ static void updateBullet(u8 i){ return; } if(offScreenBottom){ - killBullet(i, TRUE); + killBullet(i, FALSE); return; } if(bullets[i].clock > 0) bullets[i].updater(i); @@ -211,8 +220,8 @@ static void updateBullet(u8 i){ else if(!gameOver) collideWithPlayer(i); if(bullets[i].active){ s16 sx = getScreenX(bullets[i].pos.x, player.camera); - s16 sy = fix32ToInt(bullets[i].pos.y); - u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF; + s16 sy = F32_toInt(bullets[i].pos.y); + u8 off = BULLET_OFF; SPR_setPosition(bullets[i].image, sx - off, sy - off); bullets[i].clock++; bulletCount++; @@ -225,7 +234,7 @@ void updateBullets(){ if(killBullets){ killBullets = FALSE; 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) updateBullet(i); diff --git a/src/chrome.h b/src/chrome.h index 25ea989..9a26e40 100644 --- a/src/chrome.h +++ b/src/chrome.h @@ -1,17 +1,20 @@ -#define MAP_I 512 -#define MAP_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I) -#define MAP_PLAYER_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 1) -#define MAP_ENEMY_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 2) -#define MAP_TREASURE_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3) -#define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4) +#define MAP_I 328 +#define MAP_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I) +#define MAP_PLAYER_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 1) +#define MAP_ENEMY_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 2) +#define MAP_BOSS_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 9) +#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){ for(u8 i = 0; i < strlen(str); i++){ 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(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 + str[i] - 48), x + i, y); + 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(){ VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16); 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; } @@ -57,17 +60,17 @@ static void drawScore(){ void loadMap(){ 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(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 + 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(PAL0, 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, 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(PAL0, 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, 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(PAL0, 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, 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); for(s16 i = 0; i < ENEMY_COUNT; i++){ mapEnemyCol[i] = -1; @@ -96,7 +99,7 @@ static bool mapTileOccupied(s16 col, s16 row, s16 pRow){ static void updateMap(){ // 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 >= MAP_H) pRow = MAP_H - 1; @@ -109,10 +112,10 @@ static void updateMap(){ continue; } 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 >= 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 >= MAP_H) row = MAP_H - 1; mapNewCol[i] = col; @@ -127,10 +130,10 @@ static void updateMap(){ continue; } 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 >= 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 >= MAP_H) row = MAP_H - 1; mapNewTreasureCol[i] = col; @@ -173,7 +176,8 @@ static void updateMap(){ mapEnemyRow[i] = mapNewRow[i]; if(mapNewCol[i] < 0) 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 @@ -185,29 +189,68 @@ u8 phraseIndex[4]; s16 lastLevel; static void drawLevel(){ + if(isAttract) return; char lvlStr[4]; uintToStr(level + 1, lvlStr, 1); - VDP_drawText("LVL", 1, 8); - VDP_drawText(lvlStr, 4, 8); + VDP_setTextPalette(hudPal); + // VDP_drawText(lvlStr, 1, 7); + VDP_setTextPalette(PAL0); 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(){ - 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(imageChromeLife.tileset, LIFE_I, DMA); VDP_loadTileSet(imageChromeLife2.tileset, LIFE_I + 2, DMA); VDP_loadTileSet(mapIndicator.tileset, MAP_I, DMA); lastScore = 1; - drawScore(); - drawLives(); + if(!isAttract) drawScore(); + if(!isAttract) drawLives(); drawLevel(); } bool didGameOver; u32 gameOverClock; +static bool gameOverFading; static void doGameOver(){ 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 < 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){ @@ -238,19 +281,28 @@ static void doGameOver(){ treasureCollectedClock = 0; allTreasureCollected = FALSE; hitMessageClock = 0; - VDP_clearText(9, 5, 22); + VDP_clearText(9, 5, 23); - VDP_drawText("GAME OVER", 15, 13); - VDP_drawText("PRESS ANY BUTTON", 12, 14); + hudPal = PAL1; + hudPal = PAL1; + repaintHud(); + + VDP_drawText("GAME OVER", 15, 14); + VDP_drawText("Press Any Button", 12, 16); } +#define PAUSE_Y 15 + 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 < 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); SPR_setPalette(player.image, PAL1); + hudPal = PAL1; + hudPal = PAL1; + repaintHud(); XGM2_pause(); - VDP_drawText("PAUSE", 17, 13); + VDP_drawText("PAUSED", 17, PAUSE_Y); } 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 < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0); SPR_setPalette(player.image, PAL0); + hudPal = PAL0; + repaintHud(); XGM2_resume(); - VDP_clearText(17, 13, 5); + VDP_clearText(17, PAUSE_Y, 6); } u32 pauseClock; static void updatePause(){ - if(gameOver) return; + if(gameOver || isAttract || levelWaitClock > 0 || levelClearing) return; if(ctrl.start){ if(!isPausing){ isPausing = TRUE; @@ -282,59 +336,90 @@ static void updatePause(){ } if(paused){ if(pauseClock % 60 < 30) - VDP_drawText("PAUSE", 17, 13); + VDP_drawText("PAUSED", 17, PAUSE_Y); else - VDP_clearText(17, 13, 5); + VDP_clearText(17, PAUSE_Y, 6); pauseClock++; 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(){ updatePause(); if(gameOver && !didGameOver) doGameOver(); if(didGameOver){ 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(); + } return; } // level transition overlay if(levelClearing){ if(levelClearClock == 2){ + char numStr[12]; char lvlStr[4]; - uintToStr(level + 2, lvlStr, 1); - VDP_drawText("LEVEL ", 15, 13); - VDP_drawText(lvlStr, 21, 13); + char livesStr[4]; + score += 2048 + 1024 * level; + 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){ - VDP_clearText(15, 13, 10); + if(levelClearClock >= 230){ + 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; } - if(lastScore != score){ + if(!isAttract && lastScore != score){ lastScore = score; 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(treasureCollectedClock > 0 && levelWaitClock == 0){ if(treasureCollectedClock == 120){ - VDP_clearText(10, 5, 22); - 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); - VDP_drawText(phrase, 20 - len / 2, 5); - } - treasureCollectedClock--; - if(treasureCollectedClock == 0){ - VDP_clearText(10, 5, 22); - // check if all treasures are collected or gone + VDP_clearText(10, 5, 23); + // check if all treasures are now collected or gone bool allDone = TRUE; for(s16 j = 0; j < TREASURE_COUNT; j++){ if(treasures[j].active && treasures[j].state != TREASURE_COLLECTED){ @@ -344,26 +429,41 @@ void updateChrome(){ } if(allDone && collectedCount > 0){ 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 == 120){ - VDP_clearText(9, 5, 22); + VDP_clearText(9, 5, 23); treasureCollectedClock = 0; allTreasureCollected = FALSE; - VDP_drawText(hitMessageBullet ? "BLASTED" : "SMASHED", hitMessageBullet ? 16 : 16, 5); + VDP_drawText(hitMessageBullet ? "Got You!" : "Collision!", 20 - (hitMessageBullet ? 8 : 10) / 2, 5); } hitMessageClock--; if(hitMessageClock == 0) - VDP_clearText(9, 5, 22); + VDP_clearText(9, 5, 23); } - if(levelWaitClock == 240){ - VDP_clearText(9, 5, 22); + if(levelWaitClock == 210){ + VDP_clearText(9, 5, 23); treasureCollectedClock = 0; allTreasureCollected = FALSE; - VDP_drawText("ALL ENEMIES DESTROYED", 9, 5); + VDP_drawText("All Enemies Down!", 12, 5); } if(clock % 4 == 0) updateMap(); } \ No newline at end of file diff --git a/src/enemies.h b/src/enemies.h index 8e7b3b5..ffb15b0 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -42,19 +42,27 @@ void spawnEnemy(u8 type, u8 zone){ enemies[i].pos.x = randX; enemies[i].pos.y = randY; - enemies[i].off = 16; - enemies[i].image = SPR_addSprite(&fairySprite, - getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, fix32ToInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0)); + static const SpriteDefinition* bossSpriteDefs[4] = { &boss1Sprite, &boss2Sprite, &boss3Sprite, &boss4Sprite }; + SpriteDefinition const* spriteDef; + 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){ enemies[i].active = FALSE; return; } + SPR_setDepth(enemies[i].image, (type == ENEMY_TYPE_BOSS) ? 1 : 2); SPR_setVisibility(enemies[i].image, HIDDEN); enemies[i].hp = 1; for(u8 j = 0; j < PROP_COUNT; j++){ enemies[i].ints[j] = 0; + enemies[i].fixes[j] = 0; } enemies[i].ints[3] = -1; + enemies[i].anim = 0; switch(enemies[i].type){ case ENEMY_TYPE_TEST: loadEnemyOne(i); @@ -75,9 +83,9 @@ void spawnEnemy(u8 type, u8 zone){ loadBoss(i); break; } - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); - + enemies[i].vel.x = F32_mul(F32_cos(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){ @@ -92,21 +100,39 @@ static void boundsEnemy(u8 i){ } // carrying: only check for reaching the top else if(enemies[i].pos.y <= FIX32(0)){ - if(treasures[h].active) killTreasure(h); - enemies[i].ints[3] = -1; - treasureBeingCarried = FALSE; - if(enemies[i].type == ENEMY_TYPE_BUILDER){ - u8 zone = fix32ToInt(enemies[i].pos.x) / 512; - spawnEnemy(ENEMY_TYPE_GUNNER, zone); + if(isAttract){ + // in attract mode enemies can't die -- drop treasure and head back down + if(treasures[h].active){ + treasures[h].state = TREASURE_FALLING; + treasures[h].carriedBy = -1; + 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; } } else { // 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)) - enemies[i].vel.y *= -1; + if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off)){ + 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){ @@ -118,8 +144,8 @@ static void boundsEnemy(u8 i){ } static void updateEnemy(u8 i){ - enemies[i].pos.x += enemies[i].vel.x; - enemies[i].pos.y += enemies[i].vel.y; + enemies[i].pos.x += enemies[i].vel.x - (player.vel.x >> 3); + enemies[i].pos.y += enemies[i].vel.y - (playerScrollVelY >> 3); boundsEnemy(i); if(!enemies[i].active) return; @@ -169,6 +195,7 @@ static void updateEnemy(u8 i){ bullets[expSlot].frame = 0; bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0)); if(bullets[expSlot].image){ + 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); @@ -180,24 +207,29 @@ static void updateEnemy(u8 i){ enemies[i].hp = 0; killEnemy(i); } - player.lives--; - if(player.lives == 0){ - gameOver = TRUE; - XGM2_stop(); - } else { - player.respawnClock = 120; - SPR_setVisibility(player.image, HIDDEN); - killBullets = TRUE; - hitMessageClock = 120; - hitMessageBullet = FALSE; + 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 = FALSE; + } } } } 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_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); enemies[i].clock++; diff --git a/src/enemytypes.h b/src/enemytypes.h index e5078bc..7c38add 100644 --- a/src/enemytypes.h +++ b/src/enemytypes.h @@ -1,41 +1,150 @@ -// ============================================================================= -// --- Type 0: Test / Fairy (EnemyOne) --- -// ============================================================================= -// The original enemy type. A fairy that shoots 8-bullet circular bursts and -// also seeks/abducts treasures (same carry behavior as Builder). -// -// Behavior: -// - When NOT carrying: drifts at speed 2, shoots 8 bullets in a circle -// every 20 frames (only when on screen). Also scans for nearby walking -// treasures every 30 frames and steers toward the closest one within 256px. -// Grabs the treasure when within 16px. -// - When carrying: flies upward (angle 704-832, roughly up-left to up-right) -// at speed 2. Skips all shooting. boundsEnemy() handles reaching the top -// (kills treasure, self-destructs -- does NOT spawn a Gunner unlike Builder). -// -// ints[0] = random shot timer offset (0-59), desynchronizes shooting from -// other enemies so they don't all fire on the same frame -// ints[2] = target treasure index (-1 = no target) -// ints[3] = carried treasure index (-1 = not carrying) -// -// Speed: 2 HP: 1 Shoots: yes (8-bullet radial, every 20 frames) -// Abducts: yes (only 1 treasure globally at a time via treasureBeingCarried flag) -// ============================================================================= +static u8 getBossPhase(u8 i, u8 numPhases){ + s16 maxHp = enemies[i].ints[4]; + if(maxHp <= 0) return 0; + s16 lost = maxHp - enemies[i].hp; + if(lost < 0) lost = 0; + u8 phase = (lost * numPhases) / maxHp; + if(phase >= numPhases) phase = numPhases - 1; + return phase; +} + +static fix16 enemyHoneAngle(u8 i){ + fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); + fix32 dy = player.pos.y - enemies[i].pos.y; + return getAngle(dx, dy); +} + +// drone -- aim travel with triad shot +void loadDrone(u8 i){ + enemies[i].ints[0] = random() % 60; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(1 + (random() % 2)); + enemies[i].anim = random() % 3; +} +static void moveDrone(u8 i){ + enemies[i].ints[1]++; + if(enemies[i].ints[1] != 30) return; + enemies[i].ints[1] = 0; + enemies[i].angle = enemyHoneAngle(i); + if(player.respawnClock > 0) enemies[i].angle = F16_normalizeAngle(enemies[i].angle + FIX16(180)); + enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed); + enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed); + SPR_setFrame(enemies[i].image, (u8)((F16_toInt(enemies[i].angle) / 45 + 1) % 8)); +} +static void shootDrone(u8 i){ + if(level == 0) return; + if(!enemies[i].onScreen || enemies[i].clock % 60 != (u32)(enemies[i].ints[0])) return; + sfxEnemyShotC(); + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 0, + .speed = FIX32(3), + .angle = FIX16(random() % 360), + }; + + if(level >= 12){ + spawner.anim = 1; + spawner.speed = FIX32(4.5); + for(u8 j = 0; j < 4; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = angleAdd(spawner.angle, FIX16(90)); + } + } else if(level >= 9){ + spawner.anim = 1; + spawner.speed = FIX32(4); + for(u8 j = 0; j < 4; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = angleAdd(spawner.angle, FIX16(90)); + } + } else if(level >= 6){ + spawner.anim = 1; + spawner.speed = FIX32(3.5); + for(u8 j = 0; j < 4; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = angleAdd(spawner.angle, FIX16(90)); + } + } else if(level >= 3){ + spawner.anim = 1; + spawner.speed = FIX32(3.5); + for(u8 j = 0; j < 3; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = angleAdd(spawner.angle, FIX16(120)); + } + } else { + for(u8 j = 0; j < 3; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = angleAdd(spawner.angle, FIX16(120)); + } + } + + + +} +void updateDrone(u8 i){ + moveDrone(i); + shootDrone(i); +} + +// gunner -- drifts with aim burst +void loadGunner(u8 i){ + enemies[i].ints[0] = random() % 2; + enemies[i].ints[1] = random() % 40; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(2); + enemies[i].hp = 2; +} +void updateGunner(u8 i){ + if(!enemies[i].onScreen || enemies[i].clock % 40 != (u32)(enemies[i].ints[1])) return; + sfxEnemyShotA(); + fix16 aimAngle = enemyHoneAngle(i); + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 6, + .speed = FIX32(4), + .angle = aimAngle - FIX16(28), + }; + for(u8 j = 0; j < 3; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = angleAdd(spawner.angle, FIX16(28)); + } +} + +// boss one +// static void bossPatternOne(u8 i){ +// if(enemies[i].clock % 60 < 30 && enemies[i].clock % 10 == 0){ +// if(enemies[i].clock % 60 == 0) enemies[i].ints[1] = FIX16(random() % 9); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = enemies[i].clock % 20 == 0 ? 0 : 1, +// .speed = FIX32(3), +// .angle = enemies[i].ints[1] +// }; +// for(u8 j = 0; j < 6; j++){ +// spawnBullet(spawner, EMPTY); +// spawner.angle = angleAdd(spawner.angle, FIX16(60)); +// } +// enemies[i].ints[1] = angleAdd(enemies[i].ints[1], FIX16(9)); +// sfxEnemyShotA(); +// } +// } + +// test enemy with grabbin void loadEnemyOne(u8 i){ enemies[i].ints[0] = random() % 60; enemies[i].ints[2] = -1; // target treasure index enemies[i].ints[3] = -1; // carried treasure index - enemies[i].angle = ((random() % 4) * 256) + 128; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); enemies[i].speed = FIX32(2); } void updateEnemyOne(u8 i){ // carrying behavior: move upward, skip shooting if(enemies[i].ints[3] >= 0){ - // enemies[i].vel.x = (enemies[i].vel.x > 0) ? FIX32(0.3) : FIX32(-0.3); - // enemies[i].vel.y = FIX32(-1.5); - enemies[i].angle = 704 + (random() % 128); - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].angle = FIX16(248 + (random() % 45)); + enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed); + enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed); return; } @@ -71,11 +180,9 @@ void updateEnemyOne(u8 i){ fix32 dy = treasures[t].pos.y - enemies[i].pos.y; // hone toward treasure's current position at base speed - s16 angle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(treasures[t].pos.x), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(treasures[t].pos.y)); - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed); + fix16 angle = getAngle(dx, dy); + enemies[i].vel.x = F32_mul(F32_cos(angle), enemies[i].speed); + enemies[i].vel.y = F32_mul(F32_sin(angle), enemies[i].speed); // grab check: within 16px fix32 adx = dx < 0 ? -dx : dx; @@ -99,7 +206,7 @@ void updateEnemyOne(u8 i){ .y = enemies[i].pos.y, .anim = 3 + (random() % 3), .speed = FIX32(4) + FIX32(random() % 4), - .angle = random() % 128, + .angle = FIX16(random() % 45), }; switch(random() % 3){ case 1: spawner.anim += 3; break; @@ -107,149 +214,7 @@ void updateEnemyOne(u8 i){ } for(u8 j = 0; j < 8; j++){ spawnBullet(spawner, EMPTY); - spawner.angle += 128; - } - } -} - -// ============================================================================= -// --- Type 1: Drone --- -// ============================================================================= -// Bread-and-butter pressure enemy. Periodically recalculates heading toward -// the player and fires single aimed bullets. The main "fodder" type -- use -// high counts (8-16) to create constant movement pressure without overwhelming -// bullet density. -// -// Behavior: -// - Recalculates heading toward player every 30 frames (ints[1] counter). -// Between recalcs, travels in a straight line at speed 2. -// - Fires 1 aimed bullet at player every 40 frames. Only shoots when: -// a) on screen, AND b) level index >= 1 (i.e. L2+, so L1 drones are -// harmless). This is hardcoded, separate from LevelDef.dronesShoot. -// - Bounces off top/bottom screen edges. -// -// ints[0] = random shot timer offset (0-59), prevents synchronized volleys -// ints[1] = heading recalculation timer (counts up to 30) -// -// Speed: 2 HP: 1 Shoots: yes (1 aimed bullet, every 40 frames, L2+ only) -// Abducts: no -// ============================================================================= -void loadDrone(u8 i){ - enemies[i].ints[0] = random() % 60; - enemies[i].ints[1] = 0; - enemies[i].angle = random() % 1024; - enemies[i].speed = FIX32(2); -} -void updateDrone(u8 i){ - // recalculate heading toward player every 30 frames - enemies[i].ints[1]++; - if(enemies[i].ints[1] >= 30){ - enemies[i].ints[1] = 0; - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - enemies[i].angle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - if(player.respawnClock > 0) enemies[i].angle = (enemies[i].angle + 512) % 1024; - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); - } - // shooting: 1 aimed bullet every ~40 frames (only if level >= 1 i.e. L2+) - if(level >= 1 && enemies[i].onScreen && - enemies[i].clock % 40 == (u32)(enemies[i].ints[0]) % 40){ - sfxEnemyShotC(); - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - s16 aimAngle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, - .y = enemies[i].pos.y, - .anim = 3 + (random() % 9), - .speed = FIX32(4), - .angle = aimAngle, - }; - spawnBullet(spawner, EMPTY); - } -} - -// ============================================================================= -// --- Type 2: Gunner --- -// ============================================================================= -// Danmaku / bullet geometry enemy. Drifts very slowly (speed 0.5), acting more -// like a turret than a chaser. Only fires when on screen. The bullet pattern is -// set by LevelDef.gunnerPattern (written to ints[0] by loadLevel). -// -// Behavior: -// - Drifts at speed 0.5 in a random initial direction. Bounces off top/bottom. -// - Does nothing when off screen (early return). -// - Pattern 0 (Radial Burst): fires 8 bullets in a circle every 60 frames. -// Start angle rotates by 24 each volley, creating a spiral-over-time effect. -// Bullet speed 3. Predictable, good for learning dodge patterns. -// - Pattern 1 (Aimed Fan): fires 5 bullets aimed at player, spread across -// +-64 angle units, every 45 frames. Bullet speed 3. More aggressive and -// targeted -- harder to dodge at close range. -// - Pattern is set per-gunner at level load. gunnerPattern=2 means each -// gunner randomly picks 0 or 1, creating mixed bullet fields. -// -// ints[0] = pattern type (0=radial, 1=aimed fan). Set by loadLevel, not random. -// ints[1] = random shot timer offset (0-59) -// ints[2] = angle accumulator (radial pattern only, rotates start angle) -// -// Speed: 0.5 HP: 1 Shoots: yes (pattern-dependent, see above) -// Abducts: no -// ============================================================================= -void loadGunner(u8 i){ - enemies[i].ints[0] = random() % 2; - enemies[i].ints[1] = random() % 60; - enemies[i].ints[2] = 0; - enemies[i].angle = random() % 1024; - enemies[i].speed = FIX32(0.5); -} -void updateGunner(u8 i){ - if(!enemies[i].onScreen) return; - if(enemies[i].ints[0] == 0){ - // Pattern 0: Radial Burst - 8 bullets every ~60 frames - if(enemies[i].clock % 60 == (u32)(enemies[i].ints[1]) % 60){ - sfxEnemyShotB(); - s16 baseAngle = enemies[i].ints[2]; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, - .y = enemies[i].pos.y, - .anim = 3 + (random() % 3), - .speed = FIX32(3), - .angle = baseAngle, - }; - for(u8 j = 0; j < 8; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += 128; - } - enemies[i].ints[2] += 24; - if(enemies[i].ints[2] >= 1024) enemies[i].ints[2] -= 1024; - } - } else { - // Pattern 1: Aimed Fan - 5 bullets spread +-64 every ~45 frames - if(enemies[i].clock % 45 == (u32)(enemies[i].ints[1]) % 45){ - sfxEnemyShotA(); - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - s16 aimAngle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, - .y = enemies[i].pos.y, - .anim = 6 + (random() % 3), - .speed = FIX32(3), - .angle = aimAngle - 64, - }; - for(u8 j = 0; j < 5; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += 32; - } + spawner.angle = angleAdd(spawner.angle, FIX16(45)); } } } @@ -279,18 +244,14 @@ void updateGunner(u8 i){ // - Hunters are the anti-camping enemy: you can't sit still // ============================================================================= void loadHunter(u8 i){ - enemies[i].angle = random() % 1024; + enemies[i].angle = FIX16(random() % 360); enemies[i].speed = FIX32(5); } void updateHunter(u8 i){ - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - enemies[i].angle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - if(player.respawnClock > 0) enemies[i].angle = (enemies[i].angle + 512) % 1024; - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].angle = enemyHoneAngle(i); + if(player.respawnClock > 0) enemies[i].angle = F16_normalizeAngle(enemies[i].angle + FIX16(180)); + enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed); + enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed); } // ============================================================================= @@ -330,16 +291,16 @@ void loadBuilder(u8 i){ enemies[i].ints[0] = random() % 60; enemies[i].ints[2] = -1; enemies[i].ints[3] = -1; - enemies[i].angle = random() % 1024; + enemies[i].angle = FIX16(random() % 360); enemies[i].speed = FIX32(0.7); } void updateBuilder(u8 i){ // carrying: steer upward if(enemies[i].ints[3] >= 0){ - enemies[i].angle = 704 + (random() % 128); + enemies[i].angle = FIX16(248 + (random() % 45)); enemies[i].speed = FIX32(1.4); - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed); + enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed); return; } @@ -374,11 +335,9 @@ void updateBuilder(u8 i){ fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x); fix32 dy = treasures[t].pos.y - enemies[i].pos.y; enemies[i].speed = FIX32(1.4); - s16 angle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(treasures[t].pos.x), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(treasures[t].pos.y)); - enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed); - enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed); + fix16 angle = getAngle(dx, dy); + enemies[i].vel.x = F32_mul(F32_cos(angle), enemies[i].speed); + enemies[i].vel.y = F32_mul(F32_sin(angle), enemies[i].speed); // grab check fix32 adx = dx < 0 ? -dx : dx; @@ -412,137 +371,572 @@ void updateBuilder(u8 i){ // selections and timings per phase. // // ints[0] = boss number (0-4), selects which updateBoss variant runs +// ints[1] = pattern scratch (shared across patterns, reset per phase) +// ints[2] = pattern scratch (shared across patterns, reset per phase) // ints[4] = max HP (stored at load for phase calculation) +// ints[5] = Y target clock (used by updateBoss movement) +// ints[6] = Y target position (used by updateBoss movement) +// ints[7] = pattern scratch (shared across patterns) // -// Boss 1 (L4): 25 HP, 2 phases -- radial, then aimed fan -// Boss 2 (L8): 50 HP, 3 phases -- radial, aimed fan, spiral -// Boss 3 (L12): 75 HP, 4 phases -- radial, spiral, aimed fan, double radial -// Boss 4 (L16): 100 HP, 5 phases -- adds wide spray -// Boss 5 (L20): 125 HP, 6 phases -- adds ring burst (all patterns used) +// Boss 0 (L3): 24 HP, 2 phases -- One, Two +// Boss 1 (L6): 50 HP, 3 phases -- One, Two, Three +// Boss 2 (L9): 75 HP, 4 phases -- Two, Three, Four, Five +// Boss 3 (L12): 100 HP, 5 phases -- Two, Three, Four, Five, Six +// Boss 4 (L15): 125 HP, 6 phases -- Three, Four, Five, Six, Seven, Eight // -// Available boss attack patterns: -// Radial: N bullets in even circle, random start angle. Predictable. -// Aimed Fan: N bullets in arc aimed at player. Targeted. -// Spiral: N bullets in circle, start angle rotates with clock. Mesmerizing. -// Double Radial: Two offset rings (outer fast, inner slow). Dense. -// Wide Spray: N bullets in random spread toward player. Chaotic. -// Ring Burst: N bullets in even circle (like radial but different anim). Climactic. +// 8 numbered attack patterns (bossPatternOne-Eight), easiest to hardest: +// Three: Random spray, low density +// One: Rotating 6-bullet radial bursts +// Four: Sweeping aimed double-shot +// Six: 8-bullet offset radial every 10 frames +// Two: Twin aimed spiral arms +// Five: Heavy random spray, two fire windows +// Seven: Bouncing V-shots + aimed spray +// Eight: Constant double spiral + 5-bullet radial rings // // Speed: 1 HP: varies Shoots: yes (phase-dependent) Abducts: no // ============================================================================= -// shared pattern functions -static void bossPatternRadial(u8 i, u8 count, fix32 speed){ - sfxEnemyShotB(); - s16 baseAngle = random() % 1024; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle, - }; - s16 step = 1024 / count; - for(u8 j = 0; j < count; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += step; + + +// static void bossPatternTwo(u8 i){ +// if(enemies[i].clock % 60 < 50 && enemies[i].clock % 10 == 0){ +// if(enemies[i].clock % 60 == 0 || enemies[i].ints[1] == 0){ +// enemies[i].ints[1] = enemyHoneAngle(i); +// enemies[i].ints[2] = 0; +// } +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x + F32_mul(F32_cos(enemies[i].ints[1] + FIX16(90)), FIX32(12)), +// .y = enemies[i].pos.y + F32_mul(F32_sin(enemies[i].ints[1] + FIX16(90)), FIX32(12)), +// .anim = 3, +// .speed = FIX32(4), +// .angle = enemies[i].ints[1] + enemies[i].ints[2] +// }; +// spawnBullet(spawner, EMPTY); +// spawner.x = enemies[i].pos.x + F32_mul(F32_cos(enemies[i].ints[1] - FIX16(90)), FIX32(12)); +// spawner.y = enemies[i].pos.y + F32_mul(F32_sin(enemies[i].ints[1] - FIX16(90)), FIX32(12)); +// spawner.angle = enemies[i].ints[1] - enemies[i].ints[2]; +// spawnBullet(spawner, EMPTY); +// enemies[i].ints[2] = angleAdd(enemies[i].ints[2], FIX16(11.25)); +// sfxEnemyShotB(); +// } +// } + +// static void bossPatternThree(u8 i){ +// if(enemies[i].clock % 60 < 40 && enemies[i].clock % 2 == 0){ +// if(enemies[i].clock % 10 == 0) sfxEnemyShotA(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = (random() % 2) == 0 ? 0 : 1, +// .frame = 1, +// .speed = FIX32(3 + (random() % 2)), +// .angle = FIX16(random() % 360) +// }; +// spawnBullet(spawner, EMPTY); +// } +// } + +// static void bossPatternFour(u8 i){ +// if(enemies[i].clock % 60 < 40 && enemies[i].clock % 5 == 0){ +// if(enemies[i].clock % 60 == 0) +// enemies[i].ints[7] = enemyHoneAngle(i) - FIX16(45); +// if(enemies[i].clock % 10 == 0) sfxEnemyShotB(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = 0, +// .frame = 0, +// .speed = FIX32(4 + (random() % 2)), +// .angle = enemies[i].ints[7] +// }; +// spawnBullet(spawner, EMPTY); +// spawner.speed -= FIX32(1); +// spawner.anim++; +// spawnBullet(spawner, EMPTY); +// } +// enemies[i].angle = enemyHoneAngle(i); +// enemies[i].ints[7] = angleAdd(enemies[i].ints[7], FIX16(3)); +// } + +// static void bossPatternFive(u8 i){ +// if((enemies[i].clock % 60 < 30 && enemies[i].clock % 2 == 0) || (enemies[i].clock % 60 >= 35 && enemies[i].clock % 60 < 55)){ +// if(enemies[i].clock % 10 == 0) sfxEnemyShotA(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = (random() % 2) == 0 ? 0 : 1, +// .frame = 2, +// .speed = FIX32(4 + (random() % 2)), +// .angle = FIX16(random() % 360) +// }; +// spawnBullet(spawner, EMPTY); +// } +// } + +// static void bossPatternSix(u8 i){ +// if(enemies[i].clock % 60 < 40 && enemies[i].clock % 10 == 0){ +// sfxEnemyShotB(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x + F32_mul(F32_cos(enemies[i].angle), FIX32(32)), +// .y = enemies[i].pos.y + F32_mul(F32_sin(enemies[i].angle), FIX32(32)), +// .anim = enemies[i].clock % 20 == 0 ? 1 : 0, +// .frame = 1, +// .speed = FIX32(enemies[i].clock % 20 == 0 ? 3 : 4), +// .angle = FIX16(random() % 45) +// }; +// for(u8 j = 0; j < 8; j++){ +// spawnBullet(spawner, EMPTY); +// spawner.angle = angleAdd(spawner.angle, FIX16(45)); +// } +// } +// } + +// static void bossPatternSeven(u8 i){ +// if(enemies[i].clock % 60 < 30 && enemies[i].clock % 10 == 0){ +// if(enemies[i].clock % 60 == 0){ +// enemies[i].ints[7] = F32_toInt(enemies[i].pos.x); +// enemies[i].ints[2] = -16; +// } +// sfxEnemyShotA(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = 6, +// .frame = 0, +// .speed = FIX32(7) +// }; +// void updater(u8 j){ +// if(!bullets[j].ints[5]){ +// if(bullets[j].pos.y <= 8){ +// bullets[j].pos.y = FIX32(8); +// bullets[j].vel.y *= -1; +// bullets[j].vel.x = bullets[j].vel.x >> 1; +// bullets[j].vel.y = bullets[j].vel.y >> 1; +// bullets[j].angle = bullets[j].vel.x > 0 ? FIX16(45) : FIX16(135); +// doBulletRotation(j); +// bullets[j].ints[5] = TRUE; +// } else if(bullets[j].pos.y >= FIX32(216)){ +// bullets[j].pos.y = FIX32(216); +// bullets[j].vel.y *= -1; +// bullets[j].vel.x = bullets[j].vel.x >> 1; +// bullets[j].vel.y = bullets[j].vel.y >> 1; +// bullets[j].angle = bullets[j].vel.x > 0 ? FIX16(315) : FIX16(225); +// doBulletRotation(j); +// bullets[j].ints[5] = TRUE; +// } +// } +// } +// spawner.angle = player.pos.x >= enemies[i].pos.x ? FIX16(45) : FIX16(135); +// spawnBullet(spawner, updater); +// spawner.angle = player.pos.x >= enemies[i].pos.x ? FIX16(315) : FIX16(225); +// spawnBullet(spawner, updater); +// enemies[i].ints[2] += 8; +// } else if(enemies[i].clock % 60 >= 30 && enemies[i].clock % 60 < 50 && enemies[i].clock % 2 == 0){ +// if(enemies[i].clock % 10 == 0) sfxEnemyShotB(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = enemies[i].clock % 4 == 0 ? 0 : 1, +// .frame = 1, +// .speed = FIX32(3 + (random() % 3)), +// .angle = enemyHoneAngle(i) - (enemies[i].clock % 4 < 2 ? FIX16(31) : 0) + FIX16(random() % 31) +// }; +// spawnBullet(spawner, EMPTY); +// } +// } + +// static void bossPatternEight(u8 i){ +// if(enemies[i].clock % 8 == 0){ +// if(enemies[i].clock % 16 == 0) sfxEnemyShotA(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = 6, +// .frame = 0, +// .angle = enemies[i].ints[1], +// .speed = FIX32(4) +// }; +// spawnBullet(spawner, EMPTY); +// spawner.angle = angleAdd(spawner.angle, FIX16(180)); +// spawnBullet(spawner, EMPTY); +// enemies[i].ints[1] = angleAdd(enemies[i].ints[1], FIX16(19.69)); +// sfxEnemyShotA(); +// } else if(enemies[i].clock % 16 == 4){ +// sfxEnemyShotB(); +// struct bulletSpawner spawner = { +// .x = enemies[i].pos.x, +// .y = enemies[i].pos.y, +// .anim = 1, +// .frame = 0, +// .angle = 0, +// .speed = FIX32(3) +// }; +// if(enemies[i].clock % 32 == 4){ +// spawner.angle = angleAdd(spawner.angle, FIX16(36)); +// spawner.anim = 0; +// } +// for(u8 j = 0; j < 5; j++){ +// spawnBullet(spawner, EMPTY); +// spawner.angle = angleAdd(spawner.angle, FIX16(72)); +// } +// } +// } + + + + +static void bossPatternOne(u8 i){ + if(enemies[i].clock % 90 < 30 && enemies[i].clock % 10 == 0){ + if(enemies[i].clock % 90 == 0){ + enemies[i].fixes[0] = FIX16(random() % 72); + enemies[i].ints[1] = F32_toInt(enemies[i].pos.x); + enemies[i].ints[2] = F32_toInt(enemies[i].pos.y); + } + struct bulletSpawner spawner = { + .x = FIX32(enemies[i].ints[1]), + .y = FIX32(enemies[i].ints[2]), + .anim = 3, + .speed = FIX32(2.5), + .angle = enemies[i].fixes[0] + }; + for(u8 j = 0; j < 5; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(72); + } + sfxEnemyShotA(); + } else if(enemies[i].clock % 90 == 40 || enemies[i].clock % 90 == 50){ + if(enemies[i].clock % 90 == 40) + enemies[i].ints[1] = random() % 60; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .frame = 1, + .speed = FIX32(enemies[i].clock % 90 == 40 ? 3 : 4), + .angle = FIX16(enemies[i].ints[1]) + }; + + for(u8 j = 0; j < 6; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(60); + } + sfxEnemyShotB(); } } -static void bossPatternAimedFan(u8 i, u8 count, s16 spread, fix32 speed){ - sfxEnemyShotA(); - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - s16 aimAngle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; - s16 step = (count > 1) ? (spread * 2) / (count - 1) : 0; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 9 + (random() % 3), .speed = speed, .angle = aimAngle - spread, - }; - for(u8 j = 0; j < count; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += step; +static void bossPatternTwo(u8 i){ + if(enemies[i].clock % 30 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .angle = enemyHoneAngle(i), + .speed = FIX32(3) + }; + spawner.ints[0] = random() % 72; + spawner.ints[1] = enemies[i].clock % 60 == 0 ? 0 : 1; + void updater(u8 j){ + if(bullets[j].clock > 0 && bullets[j].clock % 8 == 0){ + struct bulletSpawner spawner = { + .x = bullets[j].pos.x, + .y = bullets[j].pos.y, + .anim = 4, + .angle = FIX16(bullets[j].ints[0]), + .speed = FIX32(4) + }; + spawnBullet(spawner, EMPTY); + if(bullets[j].ints[1] == 1){ + bullets[j].ints[0] += 72; + if(bullets[j].ints[0] >= 360) + bullets[j].ints[0] -= 360; + } else { + bullets[j].ints[0] -= 72; + if(bullets[j].ints[0] <= 0) + bullets[j].ints[0] += 360; + } + } + if(bullets[j].clock == 60) killBullet(j, TRUE); + } + spawnBullet(spawner, updater); } } -static void bossPatternSpiral(u8 i, u8 count, fix32 speed){ - sfxEnemyShotC(); - s16 baseAngle = (enemies[i].clock * 17) % 1024; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 6 + (random() % 3), .speed = speed, .angle = baseAngle, - }; - s16 step = 1024 / count; - for(u8 j = 0; j < count; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += step; +static void bossPatternThree(u8 i){ + if(enemies[i].clock % 10 == 0 && enemies[i].clock % 60 < 50){ + if(enemies[i].clock % 60 == 0){ + enemies[i].fixes[0] = 0; + } + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .angle = enemies[i].clock % 120 < 60 ? enemies[i].fixes[0] : FIX16(random() % 360), + .speed = FIX32(4) + }; + for(u8 j = 0; j < 4; j++){ + if(enemies[i].clock % 120 < 60){ + spawner.anim = j % 2 == 0 ? 3 : 4; + } else { + spawner.anim = j % 2 == 0 ? 9 : 10; + } + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(90); + } + if(enemies[i].clock % 120 < 60){ + if(enemies[i].clock % 240 < 120){ + enemies[i].fixes[0] += FIX16(10); + } else { + enemies[i].fixes[0] -= FIX16(10); + } + } + } else if(enemies[i].clock % 30 == 15){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .angle = FIX16(random() % 45), + .speed = FIX32(3) + }; + for(u8 j = 0; j < 8; j++){ + spawner.frame = j % 2 == 0 ? 0 : 1; + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(45); + } } } -static void bossPatternDoubleRadial(u8 i, u8 count, fix32 speed){ - sfxEnemyShotB(); - s16 baseAngle = random() % 1024; - s16 step = 1024 / count; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle, - }; - for(u8 j = 0; j < count; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += step; - } - spawner.angle = baseAngle + step / 2; - spawner.speed = speed - FIX32(1); - spawner.anim = 6 + (random() % 3); - for(u8 j = 0; j < count; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += step; +static void bossPatternFour(u8 i){ + if(enemies[i].clock % 60 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .angle = FIX16(random() % 45), + .speed = FIX32(5.5) + }; + void updater(u8 j){ + if(bullets[j].clock >= 5 && bullets[j].clock % 5 == 0 && bullets[j].clock < 15){ + bullets[j].vel.x = F32_mul(bullets[j].vel.x, FIX32(0.67)); + bullets[j].vel.y = F32_mul(bullets[j].vel.y, FIX32(0.67)); + } + } + for(u8 j = 0; j < 8; j++){ + spawner.anim = j % 2 == 0 ? 6 : 7; + spawnBullet(spawner, updater); + spawner.angle += FIX16(45); + } + } else if(enemies[i].clock % 60 >= 10 && enemies[i].clock % 60 < 50 && enemies[i].clock % 5 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .angle = enemyHoneAngle(i) - FIX16(22.5) + FIX16(random() % 45) - FIX16(180), + .speed = FIX32(4), + .anim = enemies[i].clock % 20 == 0 ? 3 : 4 + }; + void updater(u8 j){ + if(bullets[j].clock >= 4 && bullets[j].clock % 4 == 0 && bullets[j].clock < 32){ + bullets[j].speed -= FIX32(1.5); + bullets[j].vel.x = F32_mul(F32_cos(bullets[j].angle), bullets[j].speed); + bullets[j].vel.y = F32_mul(F32_sin(bullets[j].angle), bullets[j].speed); + } + } + if(spawner.angle <= 0) spawner.angle += FIX16(360); + spawnBullet(spawner, updater); } } -static void bossPatternWideSpray(u8 i, u8 count, fix32 speed){ - sfxEnemyShotA(); - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - s16 aimAngle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 3 + (random() % 9), .speed = speed, .angle = aimAngle, - }; - for(u8 j = 0; j < count; j++){ - spawner.angle = aimAngle - 128 + (random() % 256); - spawner.speed = speed - FIX32(random() % 2); - spawnBullet(spawner, EMPTY); +static void bossPatternFive(u8 i){ + if(enemies[i].clock % 60 < 30 && enemies[i].clock % 10 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = enemies[i].clock % 30 == 0 ? 7 : 6, + .angle = FIX16(30) + FIX16(random() % 10), + .speed = FIX32(4) + }; + spawner.ints[1] = random() % 2; + void updater(u8 j){ + if(bullets[j].ints[0] == 0 && (bullets[j].pos.y <= FIX32(8) || bullets[j].pos.y >= (GAME_H_F - FIX32(8)))){ + bullets[j].ints[0] = 1; + bullets[j].angle = F16_normalizeAngle(FIX16(360) - bullets[j].angle); + bullets[j].vel.x = F32_mul(F32_cos(bullets[j].angle), bullets[j].speed); + bullets[j].vel.y = F32_mul(F32_sin(bullets[j].angle), bullets[j].speed); + doBulletRotation(j); + } else if(bullets[j].ints[0] == 1 && bullets[j].clock % 8 == 0){ + bullets[j].angle += FIX16(bullets[j].ints[1] == 0 ? 2 : -2); + bullets[j].vel.x = F32_mul(F32_cos(bullets[j].angle), bullets[j].speed); + bullets[j].vel.y = F32_mul(F32_sin(bullets[j].angle), bullets[j].speed); + doBulletRotation(j); + } + } + for(u8 j = 0; j < 6; j++){ + spawnBullet(spawner, updater); + spawner.angle += FIX16(j == 2 ? 90 : 45); + } + } else if(enemies[i].clock % 60 == 40){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .angle = FIX16(random() % 45), + .speed = FIX32(3) + }; + for(u8 j = 0; j < 8; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(45); + } } } -static void bossPatternRingBurst(u8 i, u8 count, fix32 speed){ - sfxEnemyShotB(); - sfxEnemyShotC(); - s16 baseAngle = random() % 1024; - s16 step = 1024 / count; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 9 + (random() % 3), .speed = speed, .angle = baseAngle, - }; - for(u8 j = 0; j < count; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += step; +static void bossPatternSix(u8 i){ + if(enemies[i].clock % 60 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 2, + .frame = 0, + .angle = FIX16(random() % 45), + .speed = FIX32(3) + }; + void updater(u8 j){ + if(bullets[j].clock % 10 == 0 && bullets[j].clock > 0){ + bullets[j].angle = F16_normalizeAngle(bullets[j].angle + FIX16(bullets[j].ints[0] == 0 ? 10 : -10)); + updateBulletVel(j); + } + } + for(u8 j = 0; j < 8; j++){ + spawner.ints[0] = 0; + spawnBullet(spawner, updater); + spawner.ints[0] = 1; + spawnBullet(spawner, updater); + spawner.angle += FIX16(45); + } + } else if(enemies[i].clock % 60 == 20 || enemies[i].clock % 60 == 30 || enemies[i].clock % 60 == 40){ + if(enemies[i].clock % 60 == 20){ + enemies[i].fixes[0] = random() % FIX16(22.5); + } + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = enemies[i].clock % 60 == 30 ? 1 : 0, + .frame = 1, + .angle = enemies[i].fixes[0] + FIX16(enemies[i].clock % 60 == 30 ? 22.5 : 0), + .speed = FIX32(4) + }; + for(u8 j = 0; j < 8; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(45); + } } } -// get current phase based on HP. phase 0 = full health, higher = more damaged -static u8 getBossPhase(u8 i, u8 numPhases){ - s16 maxHp = enemies[i].ints[4]; - if(maxHp <= 0) return 0; - s16 lost = maxHp - enemies[i].hp; - if(lost < 0) lost = 0; - u8 phase = (lost * numPhases) / maxHp; - if(phase >= numPhases) phase = numPhases - 1; - return phase; +static void bossPatternSeven(u8 i){ + if(enemies[i].clock % 3 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 0, + .frame = 0, + .angle = FIX16(random() % 360), + .speed = FIX32(4) + }; + if(enemies[i].clock % 6 == 0) + spawner.frame = 1; + void updater(u8 j){ + if(bullets[j].ints[0] == 0 && (bullets[j].pos.y <= FIX32(8) || bullets[j].pos.y >= (GAME_H_F - FIX32(8)))){ + bullets[j].ints[0] = TRUE; + bullets[j].anim = 1; + bullets[j].vel.y = F32_mul(bullets[j].vel.y, FIX32(-0.5)); + SPR_setAnim(bullets[j].image, bullets[j].anim); + SPR_setFrame(bullets[j].image, bullets[j].frame); + } + } + spawnBullet(spawner, updater); + } +} + +static void bossPatternEight(u8 i){ + if(enemies[i].clock % 60 >= 30 && enemies[i].clock % 60 < 34){ + if(enemies[i].clock % 60 == 30){ + enemies[i].ints[1] = F32_toInt(enemies[i].pos.x); + enemies[i].ints[2] = F32_toInt(enemies[i].pos.y); + enemies[i].fixes[0] = enemyHoneAngle(i) - FIX16(30); + enemies[i].fixes[2] = FIX16(0); + } + struct bulletSpawner spawner = { + .x = FIX32(enemies[i].ints[1]), + .y = FIX32(enemies[i].ints[2]), + .anim = 1, + .frame = 0, + .angle = enemies[i].fixes[0], + .speed = FIX32(2.5) + F16_toFix32(enemies[i].fixes[2]) + }; + for(u8 j = 0; j < 3; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(30); + } + enemies[i].fixes[2] += FIX16(0.5); + } else if(enemies[i].clock % 60 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 2, + .frame = 1, + .angle = FIX16(random() % 45), + .speed = FIX32(3) + }; + for(u8 j = 0; j < 8; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(45); + } + } +} + +static void updateBossOne(u8 i){ + bossPatternEight(i); + // u8 phase = getBossPhase(i, 2); + // if(phase == 0) bossPatternOne(i); + // else bossPatternTwo(i); +} + + + + +// Boss 1 (L6): 3 phases, 50 HP +static void updateBossTwo(u8 i){ + u8 phase = getBossPhase(i, 3); + if(phase == 0) bossPatternOne(i); + else if(phase == 1) bossPatternTwo(i); + else bossPatternThree(i); +} + +// Boss 2 (L9): 4 phases, 75 HP +static void updateBossThree(u8 i){ + u8 phase = getBossPhase(i, 4); + if(phase == 0) bossPatternTwo(i); + else if(phase == 1) bossPatternThree(i); + else if(phase == 2) bossPatternFour(i); + else bossPatternFive(i); +} + +// Boss 3 (L12): 5 phases, 100 HP +static void updateBossFour(u8 i){ + u8 phase = getBossPhase(i, 5); + if(phase == 0) bossPatternTwo(i); + else if(phase == 1) bossPatternThree(i); + else if(phase == 2) bossPatternFour(i); + else if(phase == 3) bossPatternFive(i); + else bossPatternSix(i); +} + +// Boss 4 (L15): 6 phases, 125 HP +static void updateBossFive(u8 i){ + u8 phase = getBossPhase(i, 6); + if(phase == 0) bossPatternThree(i); + else if(phase == 1) bossPatternFour(i); + else if(phase == 2) bossPatternFive(i); + else if(phase == 3) bossPatternSix(i); + else if(phase == 4) bossPatternSeven(i); + else bossPatternEight(i); } void loadBoss(u8 i){ @@ -552,81 +946,65 @@ void loadBoss(u8 i){ pendingBossNum = 0; enemies[i].ints[1] = 0; enemies[i].ints[4] = enemies[i].hp; - enemies[i].angle = random() % 1024; - enemies[i].speed = FIX32(1); -} - -// Boss 1 (L6): 2 patterns, 25 HP -static void updateBossOne(u8 i){ - u8 phase = getBossPhase(i, 2); - if(phase == 0){ - if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 10, FIX32(3)); - } else { - if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 6, 80, FIX32(3)); - } -} - -// Boss 2 (L12): 3 patterns, 50 HP -static void updateBossTwo(u8 i){ - u8 phase = getBossPhase(i, 3); - if(phase == 0){ - if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3)); - } else if(phase == 1){ - if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3)); - } else { - if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 6, FIX32(4)); - } -} - -// Boss 3 (L18): 4 patterns, 75 HP -static void updateBossThree(u8 i){ - u8 phase = getBossPhase(i, 4); - if(phase == 0){ - if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3)); - } else if(phase == 1){ - if(enemies[i].clock % 40 == 0) bossPatternSpiral(i, 8, FIX32(3)); - } else if(phase == 2){ - if(enemies[i].clock % 35 == 0) bossPatternAimedFan(i, 10, 112, FIX32(4)); - } else { - if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 8, FIX32(4)); - } -} - -// Boss 4 (L24): 5 patterns, 100 HP -static void updateBossFour(u8 i){ - u8 phase = getBossPhase(i, 5); - if(phase == 0){ - if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 14, FIX32(3)); - } else if(phase == 1){ - if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3)); - } else if(phase == 2){ - if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 8, FIX32(4)); - } else if(phase == 3){ - if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4)); - } else { - if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 12, FIX32(4)); - } -} - -// Boss 5 (L30): 6 patterns, 125 HP -static void updateBossFive(u8 i){ - u8 phase = getBossPhase(i, 6); - if(phase == 0){ - if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 16, FIX32(3)); - } else if(phase == 1){ - if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 10, 112, FIX32(3)); - } else if(phase == 2){ - if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 10, FIX32(4)); - } else if(phase == 3){ - if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4)); - } else if(phase == 4){ - if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 14, FIX32(5)); - } else { - if(enemies[i].clock % 20 == 0) bossPatternRingBurst(i, 16, FIX32(5)); - } + enemies[i].ints[5] = random() % 90; // Y target clock (start random offset) + enemies[i].ints[6] = 72 + (random() % 80); + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(4); } void updateBoss(u8 i){ + // Homing: recalculate heading and speed toward player every 30 frames (X-axis only) + if(enemies[i].clock % 30 == 0){ + fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); + enemies[i].angle = getAngle(dx, FIX32(112) - enemies[i].pos.y); + if(player.respawnClock > 0) enemies[i].angle = F16_normalizeAngle(enemies[i].angle + FIX16(180)); + enemies[i].speed = (dx > FIX32(-128) && dx < FIX32(128)) ? FIX32(0.5) : FIX32(3); + enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed); + } + + // Y movement: pick random target every 90 frames, move toward it with 10px dead zone + enemies[i].ints[5]++; + if(enemies[i].ints[5] >= 90){ + enemies[i].ints[5] = 0; + enemies[i].ints[6] = 72 + (random() % 80); // new Y target in [72, 151] + } + fix32 dy = FIX32(enemies[i].ints[6]) - enemies[i].pos.y; + if(dy > FIX32(10)){ + enemies[i].vel.y = FIX32(1); // move down + } else if(dy < FIX32(-10)){ + enemies[i].vel.y = FIX32(-1); // move up + } else { + enemies[i].vel.y = 0; // in dead zone + } + + // Guardrails: reflect at Y bounds 72 and 152 + if(enemies[i].pos.y < FIX32(72)){ + enemies[i].pos.y = FIX32(72); + enemies[i].vel.y = FIX32(1); // reflect down + enemies[i].ints[6] = 80; // pick new target below + } + if(enemies[i].pos.y > FIX32(152)){ + enemies[i].pos.y = FIX32(152); + enemies[i].vel.y = FIX32(-1); // reflect up + enemies[i].ints[6] = 144; // pick new target above + } + // Directional sprite: 4 orientations (right, down, left, up) with 2 frames each + fix16 a = F16_normalizeAngle(enemies[i].angle); + if(a < FIX16(45) || a >= FIX16(315)){ + SPR_setAnim(enemies[i].image, 1); + SPR_setHFlip(enemies[i].image, FALSE); + } else if(a < FIX16(135)){ + SPR_setAnim(enemies[i].image, 0); + SPR_setHFlip(enemies[i].image, FALSE); + } else if(a < FIX16(225)){ + SPR_setAnim(enemies[i].image, 1); + SPR_setHFlip(enemies[i].image, TRUE); + } else { + SPR_setAnim(enemies[i].image, 2); + SPR_setHFlip(enemies[i].image, FALSE); + } + SPR_setFrame(enemies[i].image, (enemies[i].clock / 30) % 2); + if(!enemies[i].onScreen) return; switch(enemies[i].ints[0]){ case 0: updateBossOne(i); break; diff --git a/src/global.h b/src/global.h index 4ae51bb..005695e 100644 --- a/src/global.h +++ b/src/global.h @@ -5,6 +5,7 @@ void sfxEnemyShotB(); void sfxEnemyShotC(); void sfxExplosion(); void sfxPickup(); +void sfxGraze(); void loadMap(); void loadGame(); @@ -21,12 +22,34 @@ u32 clock; #define GAME_WRAP (SECTION_SIZE * SECTION_COUNT) #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 0 +// #define MUSIC_VOLUME 50 +#define MUSIC_VOLUME 0 u32 score; +u32 highScore; +u32 tempHighScore; +u32 grazeCount; +u32 nextExtendScore; +#define EXTEND_SCORE 25000 #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 @@ -34,12 +57,20 @@ u32 score; #define MAP_Y 1 #define MAP_W 38 #define MAP_H 3 +#define MAP_SCALE (F32_toInt(GAME_WRAP) / MAP_W) void EMPTY(s16 i){(void)i;} bool started; bool gameOver; 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; u8 level; s16 pendingBossHp; @@ -59,6 +90,7 @@ struct controls { bool left, right, up, down, a, b, c, start; }; struct controls ctrl; +struct controls ctrlHW; // hardware-only copy — never overridden by AI void updateControls(u16 joy, u16 changed, u16 state){ (void)changed; // Unused parameter if(joy == JOY_1){ @@ -70,6 +102,7 @@ void updateControls(u16 joy, u16 changed, u16 state){ ctrl.b = (state & BUTTON_B); ctrl.c = (state & BUTTON_C); ctrl.start = (state & BUTTON_START); + ctrlHW = ctrl; } } @@ -79,10 +112,13 @@ struct playerStruct { Vect2D_f32 pos, vel; s16 shotAngle; 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; Sprite* image; }; struct playerStruct player; +fix32 playerScrollVelY; // player.vel.y zeroed when clamped at top/bottom bound bool killBullets; @@ -93,14 +129,17 @@ struct bulletSpawner { fix32 x, y, speed; Vect2D_f32 vel; s16 angle, anim, frame; + s16 ints[PROP_COUNT]; bool top, player; }; struct bullet { - bool active, player, vFlip, hFlip, explosion; + fix32 speed; + bool active, player, vFlip, hFlip, explosion, grazed; Vect2D_f32 pos, vel; Sprite* image; s16 clock, angle, anim, frame; s16 dist; + s16 ints[PROP_COUNT]; void (*updater)(s16); }; struct bullet bullets[BULLET_COUNT]; @@ -119,13 +158,14 @@ struct bullet bullets[BULLET_COUNT]; struct enemy { bool active, onScreen; u8 type; - s16 hp; + s16 hp, frame, anim; s16 angle, off; u32 clock; fix32 speed; Vect2D_f32 vel, pos; Sprite* image; s16 ints[PROP_COUNT]; + fix16 fixes[PROP_COUNT]; }; struct enemy enemies[ENEMY_COUNT]; @@ -148,6 +188,9 @@ struct treasure { struct treasure treasures[TREASURE_COUNT]; bool treasureBeingCarried; s16 collectedCount; +u16 levelEnemiesKilled; +u16 statEnemiesKilled; +s16 statTreasures; void killTreasure(u8 i){ if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){ @@ -160,19 +203,17 @@ void killTreasure(u8 i){ void killBullet(u8 i, bool explode){ if(explode){ + s16 a = bullets[i].anim; + s16 explosionAnim; if(bullets[i].player){ - SPR_setAnim(bullets[i].image, 1); + explosionAnim = 16; + } else if(a < FIRST_ROTATING_BULLET){ + explosionAnim = 13 + bullets[i].frame; } else { - s16 a = bullets[i].anim; - s16 explosionAnim; - if(a < FIRST_ROTATING_BULLET){ - explosionAnim = 13 + bullets[i].frame; - } else { - s16 mod = a % 3; - explosionAnim = 13 + mod; - } - SPR_setAnim(bullets[i].image, explosionAnim); + s16 mod = a % 3; + explosionAnim = 13 + mod; } + SPR_setAnim(bullets[i].image, explosionAnim); bullets[i].clock = 0; bullets[i].frame = 0; bullets[i].explosion = TRUE; @@ -186,6 +227,7 @@ void killBullet(u8 i, bool explode){ } void killEnemy(u8 i){ + if(isAttract) return; enemies[i].hp--; if(enemies[i].hp > 0) return; if(enemies[i].ints[3] >= 0){ @@ -200,6 +242,7 @@ void killEnemy(u8 i){ } enemies[i].active = FALSE; SPR_releaseSprite(enemies[i].image); + levelEnemiesKilled++; } 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) { fix32 screenX = worldX - camera; - if (screenX < FIX32(-256)) { + if (screenX < -(GAME_WRAP / 2)) { screenX += GAME_WRAP; - } else if (screenX > FIX32(256)) { + } else if (screenX > (GAME_WRAP / 2)) { screenX -= GAME_WRAP; } - return fix32ToInt(screenX); + return F32_toInt(screenX); } -// homing -#define PI_MOD 2.84444444444 -#define PI_F FIX16(3.14159265358 * PI_MOD) -#define PI_F_2 FIX16(1.57079632679 * PI_MOD) -#define PI_F_4 FIX16(0.78539816339 * PI_MOD) -fix16 arctan(fix16 x) { - return fix16Mul(PI_F_4, x) - fix16Mul(fix16Mul(x, (abs(x) - 1)), (FIX16(0.245) + fix16Mul(FIX16(0.066), abs(x)))); +// homing -- degree-based using SGDK F16_atan2 (returns fix16 degrees) +static fix16 getAngle(fix32 dx, fix32 dy){ + s16 ix = (s16)(F32_toInt(dx) >> 2); + s16 iy = (s16)(F32_toInt(dy) >> 2); + if(ix == 0 && iy == 0) return 0; + return F16_normalizeAngle(F16_atan2(FIX16(iy), FIX16(ix))); } -fix16 arctan2(fix16 y, fix16 x) { - return x >= 0 ? - (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)))) : - (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)))); + +// safe angle accumulation -- keeps angle in [-180, 180) so adding up to 180° can't overflow s16 +static s16 angleAdd(s16 a, s16 step){ + if(a >= FIX16(180)) a -= FIX16(360); + return a + step; } -s16 arcAngle; -s16 honeAngle(fix16 x1, fix16 x2, fix16 y1, fix16 y2){ - arcAngle = arctan2(y2 - y1, x2 - x1); - if(arcAngle >= 128) arcAngle -= 32; - if(arcAngle >= 384) arcAngle -= 32; - if(arcAngle < 0){ - arcAngle = 1024 + arcAngle; - if(arcAngle < 896) arcAngle += 32; - if(arcAngle < 640) arcAngle += 32; + +static bool isBossLevel(u8 lvl){ + return (lvl >= 2) && ((lvl + 1) % 3 == 0); +} + +#define FONT_THEME_RED 0 +#define FONT_THEME_GREEN 1 +#define FONT_THEME_BLUE 2 + +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); } \ No newline at end of file diff --git a/src/main.c b/src/main.c index 88c67e8..d266886 100644 --- a/src/main.c +++ b/src/main.c @@ -10,19 +10,32 @@ #include "stage.h" #include "chrome.h" #include "start.h" +#include "starfield.h" #include "sfx.h" static void loadInternals(){ JOY_init(); JOY_setEventHandler(&updateControls); - SPR_init(); VDP_setPlaneSize(128, 32, TRUE); + SPR_init(); VDP_loadFont(font.tileset, DMA); PAL_setPalette(PAL0, font.palette->data, DMA); PAL_setPalette(PAL1, shadow.palette->data, CPU); + PAL_setPalette(PAL2, shadow.palette->data, CPU); 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(){ for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) killBullet(i, FALSE); @@ -34,6 +47,7 @@ void clearLevel(){ collectedCount = 0; allTreasureCollected = FALSE; treasureCollectedClock = 0; + levelEnemiesKilled = 0; // black out everything SPR_setVisibility(player.image, HIDDEN); VDP_clearTileMapRect(BG_A, 0, 0, 128, 32); @@ -41,43 +55,90 @@ void clearLevel(){ } void loadGame(){ + VDP_setVerticalScroll(BG_A, 0); + score = 0; + nextExtendScore = EXTEND_SCORE; loadBackground(); loadPlayer(); loadChrome(); - loadLevel(0); - XGM2_play(stageMusic); - XGM2_setFMVolume(MUSIC_VOLUME); - XGM2_setPSGVolume(MUSIC_VOLUME); + loadLevel(isAttract ? ATTRACT_LEVEL : START_LEVEL); +#if MUSIC_VOLUME > 0 + if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic); +#endif player.recoveringClock = 240; + player.recoverFlash = FALSE; killBullets = TRUE; + attractEnding = FALSE; started = TRUE; + startLevelFadeIn(); } 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(); - updateSfx(); if(levelClearing){ levelClearClock++; + if(levelClearClock == 73) + XGM2_stop(); if(levelClearClock == 1){ 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; player.pos.y = FIX32(112); player.camera = player.pos.x - FIX32(160); playerVelX = 0; + clearStarfield(); loadBackground(); loadChrome(); loadLevel(level + 1); - SPR_setVisibility(player.image, VISIBLE); + startLevelFadeIn(); + player.pendingShow = TRUE; player.recoveringClock = 240; + player.recoverFlash = FALSE; killBullets = TRUE; + XGM2_stop(); +#if MUSIC_VOLUME > 0 + if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic); +#endif } + if(levelClearing) updateStarfield(); return; } if(levelWaitClock > 0){ 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){ + statEnemiesKilled = levelEnemiesKilled; + statTreasures = collectedCount; levelClearing = TRUE; levelClearClock = 0; return; @@ -93,8 +154,10 @@ static void updateGame(){ gameOver = TRUE; XGM2_stop(); } else { - levelWaitClock = 240; + levelWaitClock = 210; killBullets = TRUE; + if(paused){ paused = FALSE; clearPause(); } + XGM2_stop(); } } updateTreasures(); diff --git a/src/player.h b/src/player.h index 3ce6011..4ab019d 100644 --- a/src/player.h +++ b/src/player.h @@ -1,4 +1,4 @@ -#define PLAYER_SPEED FIX32(6) +#define PLAYER_SPEED FIX32(5.5) #define PLAYER_ACCEL PLAYER_SPEED >> 4 @@ -6,16 +6,17 @@ #define PLAYER_BOUND_Y FIX32(PLAYER_OFF) #define PLAYER_BOUND_H FIX32(224 - PLAYER_OFF) -#define CAMERA_X FIX32(96) -#define CAMERA_W FIX32(224) +#define CAMERA_X FIX32(112) +#define CAMERA_W FIX32(208) -#define SHOT_INTERVAL 15 +#define SHOT_INTERVAL 20 +#define PLAYER_SHOT_SPEED FIX32(18) s16 shotClock; fix32 screenX; fix32 playerSpeed; -fix32 playerVelX; +fix32 playerVelX; static void movePlayer(){ player.vel.y = 0; @@ -24,7 +25,7 @@ static void movePlayer(){ if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){ playerSpeed = PLAYER_SPEED; 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; } @@ -43,8 +44,8 @@ static void movePlayer(){ player.vel.x = playerVelX; if(player.vel.x != 0 && player.vel.y != 0){ - player.vel.x = fix32Mul(player.vel.x, FIX32(0.707)); - player.vel.y = fix32Mul(player.vel.y, FIX32(0.707)); + player.vel.x = F32_mul(player.vel.x, FIX32(0.707)); + player.vel.y = F32_mul(player.vel.y, FIX32(0.707)); } player.pos.x += player.vel.x; @@ -62,10 +63,14 @@ static void movePlayer(){ } 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; - 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; + if(playerScrollVelY > 0) playerScrollVelY = 0; + } if(player.pos.x >= GAME_WRAP){ player.pos.x -= GAME_WRAP; @@ -89,15 +94,18 @@ static void cameraPlayer(){ static void shootPlayer(){ if(ctrl.a && shotClock == 0){ + // fix32 bulletVelX = (player.shotAngle == 0 ? PLAYER_SHOT_SPEED : -PLAYER_SHOT_SPEED) + (player.vel.x * 3); struct bulletSpawner spawner = { .x = player.pos.x, .y = player.pos.y, - .anim = 0, - .speed = FIX32(24), + .anim = 12, + .speed = PLAYER_SHOT_SPEED, .angle = player.shotAngle, .player = TRUE }; + spawner.ints[5] = F32_toInt(player.shotAngle == 0 ? PLAYER_SHOT_SPEED : -PLAYER_SHOT_SPEED); 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); } spawnBullet(spawner, updater); @@ -106,6 +114,57 @@ static void shootPlayer(){ } 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(){ player.shotAngle = 0; player.camera = 0; @@ -113,10 +172,12 @@ void loadPlayer(){ player.pos.y = FIX32(112); playerVelX = 0; player.lives = 3; + shotClock = isAttract ? SHOT_INTERVAL : 0; player.image = SPR_addSprite(&momoyoSprite, - fix32ToInt(player.pos.x) - PLAYER_OFF, - fix32ToInt(player.pos.y) - PLAYER_OFF, + F32_toInt(player.pos.x) - PLAYER_OFF, + F32_toInt(player.pos.y) - PLAYER_OFF, TILE_ATTR(PAL0, 0, 0, 0)); + SPR_setDepth(player.image, 0); } void updatePlayer(){ @@ -140,29 +201,35 @@ void updatePlayer(){ player.pos.y += (targetY - player.pos.y) >> 3; // keep sprite position in sync so it doesn't pop on reappear 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); player.respawnClock--; if(player.respawnClock == 0){ SPR_setVisibility(player.image, VISIBLE); - player.recoveringClock = 240; + player.recoveringClock = 180; + player.recoverFlash = TRUE; killBullets = TRUE; } return; } 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); player.recoveringClock--; if(player.recoveringClock == 0) SPR_setVisibility(player.image, VISIBLE); } + if(isAttract) moveAttract(); movePlayer(); boundsPlayer(); cameraPlayer(); shootPlayer(); 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); + if(player.pendingShow){ + SPR_setVisibility(player.image, VISIBLE); + player.pendingShow = FALSE; + } } } \ No newline at end of file diff --git a/src/sfx.h b/src/sfx.h index 47209c4..8119bbb 100644 --- a/src/sfx.h +++ b/src/sfx.h @@ -1,105 +1,58 @@ -static s16 sfxShotClock; -static u16 sfxShotFreq; - -#define SFX_VOL 2 - void sfxPlayerShot(){ - sfxShotClock = 4; - sfxShotFreq = 150; - PSG_setEnvelope(2, SFX_VOL); - PSG_setFrequency(2, sfxShotFreq); + XGM2_playPCM(sfxSamplePlayerShot, sizeof(sfxSamplePlayerShot), SOUND_PCM_CH1); } -static s16 sfxEnemyShotClock; -static u16 sfxEnemyShotFreq; -static u8 sfxEnemyShotType; - -// high sharp zap - quick descending chirp void sfxEnemyShotA(){ if(player.recoveringClock > 0) return; - sfxEnemyShotClock = 3; - sfxEnemyShotFreq = 1200; - sfxEnemyShotType = 0; - PSG_setEnvelope(1, SFX_VOL); - PSG_setFrequency(1, sfxEnemyShotFreq); + XGM2_playPCM(sfxSampleBullet1, sizeof(sfxSampleBullet1), SOUND_PCM_CH2); } -// mid buzzy pulse - sits in the midrange void sfxEnemyShotB(){ if(player.recoveringClock > 0) return; - sfxEnemyShotClock = 5; - sfxEnemyShotFreq = 400; - sfxEnemyShotType = 1; - PSG_setEnvelope(1, SFX_VOL); - PSG_setFrequency(1, sfxEnemyShotFreq); + XGM2_playPCM(sfxSampleBullet2, sizeof(sfxSampleBullet2), SOUND_PCM_CH2); } -// quick rising ping - sweeps upward void sfxEnemyShotC(){ if(player.recoveringClock > 0) return; - sfxEnemyShotClock = 4; - sfxEnemyShotFreq = 300; - sfxEnemyShotType = 2; - PSG_setEnvelope(1, SFX_VOL); - PSG_setFrequency(1, sfxEnemyShotFreq); + XGM2_playPCM(sfxSampleBullet3, sizeof(sfxSampleBullet3), SOUND_PCM_CH2); } -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(){ - sfxExpClock = 18; - PSG_setNoise(PSG_NOISE_TYPE_WHITE, PSG_NOISE_FREQ_CLOCK2); - PSG_setEnvelope(3, SFX_VOL); + XGM2_playPCM(sfxSampleExplosion, sizeof(sfxSampleExplosion), SOUND_PCM_CH3); } -void updateSfx(){ - if(sfxExpClock > 0){ - sfxExpClock--; - PSG_setEnvelope(3, SFX_VOL + (18 - sfxExpClock) * (15 - SFX_VOL) / 18); - if(sfxExpClock == 0){ - PSG_setEnvelope(3, 15); - } - } - if(sfxEnemyShotClock > 0){ - sfxEnemyShotClock--; - if(sfxEnemyShotType == 0) sfxEnemyShotFreq -= 300; - else if(sfxEnemyShotType == 1) sfxEnemyShotFreq -= 50; - else sfxEnemyShotFreq += 150; - PSG_setFrequency(1, sfxEnemyShotFreq); - 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)); - if(sfxEnemyShotClock == 0){ - PSG_setEnvelope(1, 15); - } - } - if(sfxPickupClock > 0){ - sfxPickupClock--; - // rising staircase: jump up every 3 frames - if(sfxPickupClock % 3 == 0) sfxPickupFreq += 200; - PSG_setFrequency(0, sfxPickupFreq); - PSG_setEnvelope(0, SFX_VOL); - if(sfxPickupClock == 0){ - PSG_setEnvelope(0, 15); - } - } - if(sfxShotClock > 0){ - sfxShotClock--; - sfxShotFreq -= 30; - PSG_setFrequency(2, sfxShotFreq); - PSG_setEnvelope(2, SFX_VOL + (4 - sfxShotClock) * (15 - SFX_VOL) / 4); - if(sfxShotClock == 0){ - PSG_setEnvelope(2, 15); - } - } +void sfxPickup(){ + XGM2_playPCM(sfxSamplePickup, sizeof(sfxSamplePickup), SOUND_PCM_CH1); +} + +void sfxGraze(){ + XGM2_playPCM(sfxSampleGraze, sizeof(sfxSampleGraze), SOUND_PCM_CH1); +} + +void sfxStartGame(){ + XGM2_playPCM(sfxSampleStartGame, sizeof(sfxSampleStartGame), SOUND_PCM_CH1); +} + +void sfxPlayerHit(){ + XGM2_playPCM(sfxSamplePlayerHit, sizeof(sfxSamplePlayerHit), SOUND_PCM_CH3); +} + +void sfxGameOver(){ + XGM2_playPCM(sfxSampleGameOver, sizeof(sfxSampleGameOver), SOUND_PCM_CH1); +} + +void sfxMenuSelect(){ + XGM2_playPCM(sfxSampleMenuSelect, sizeof(sfxSampleMenuSelect), SOUND_PCM_CH2); +} + +void sfxMenuChoose(){ + XGM2_playPCM(sfxSampleMenuChoose, sizeof(sfxSampleMenuChoose), SOUND_PCM_CH2); +} + +void sfxCollectTreasure(){ + XGM2_playPCM(sfxSampleCollectTreasure, sizeof(sfxSampleCollectTreasure), SOUND_PCM_CH3); +} + +void sfxCollectAllTreasures(){ + XGM2_playPCM(sfxSampleCollectAllTreasures, sizeof(sfxSampleCollectAllTreasures), SOUND_PCM_CH3); } diff --git a/src/stage.h b/src/stage.h index 3b63da7..1108887 100644 --- a/src/stage.h +++ b/src/stage.h @@ -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 -// enemies are killed (enemyCount == 0). Treasures are bonus -- they don't affect -// level completion. +// Levels are procedurally generated using a threat point (TP) budget. +// Each enemy type has a TP cost, and the budget grows with level index. +// Enemy types unlock progressively. Compositions vary each playthrough +// (RNG seeded from title screen). // -// --- STRUCT FIELDS --- -// -// 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) +// Boss levels occur every 3rd level (indices 2, 5, 8, 11, 14). +// Boss escorts use 40% of normal budget, limited to drones + builders. // // ============================================================================= -struct LevelDef { - u8 drones, gunners, hunters, builders; - u8 bossHp; - u8 treasures; - u8 gunnerPattern; - bool dronesShoot; +#define LEVEL_COUNT 15 +#define TP_POOL_SIZE 5 + +// pool index -> enemy type mapping +static const u8 poolTypeMap[TP_POOL_SIZE] = { + 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 -const struct LevelDef levels[20] = { - // Phase 1: "Immediate danger" (L1-L4) - { 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1 - { 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2 - { 12, 3, 0, 0, 0, 8, 1, TRUE }, // L3 - { 8, 0, 0, 0, 25, 8, 0, TRUE }, // L4 BOSS 1 +// TP costs per pool index +static const u8 typeCost[TP_POOL_SIZE] = { 5, 2, 4, 3, 3 }; +static const u8 typeWeight[TP_POOL_SIZE] = { 2, 8, 4, 3, 3 }; +static const u8 typeMaxCount[TP_POOL_SIZE] = { 3, 16, 6, 6, 2 }; +static const u8 typeMinCount[TP_POOL_SIZE] = { 0, 2, 0, 0, 0 }; - // Phase 2: "You can't save everything" (L5-L8) - { 10, 2, 0, 1, 0, 8, 0, TRUE }, // L5 - { 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 +// Boss HP per boss number (0-4) +static const s16 bossHpTable[5] = { 24, 50, 75, 100, 125 }; - // Phase 3: "Geometry matters" (L9-L12) - { 8, 3, 4, 0, 0, 8, 1, TRUE }, // L9 - { 10, 2, 4, 0, 0, 8, 2, TRUE }, // L10 - { 12, 3, 3, 0, 0, 8, 1, TRUE }, // L11 - { 0, 2, 2, 0, 75, 8, 2, TRUE }, // L12 BOSS 3 +// Returns bitmask of unlocked pool indices for a given level +static u8 getUnlockedTypes(u8 lvl){ + u8 mask = 0; + // Drone always unlocked + 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) - { 14, 4, 0, 2, 0, 8, 2, TRUE }, // L13 - { 10, 0, 6, 0, 0, 8, 0, TRUE }, // L14 - { 12, 4, 2, 0, 0, 8, 1, TRUE }, // L15 - { 0, 3, 0, 1, 100, 8, 2, TRUE }, // L16 BOSS 4 +static u8 getTreasureCount(u8 lvl){ + if(lvl == 0) return 4; + if(lvl <= 2) return 6; + return 8; +} - // Phase 5: "Arcade cruelty" (L17-L20) - { 16, 0, 4, 0, 0, 8, 0, TRUE }, // L17 - { 14, 4, 4, 2, 0, 8, 2, TRUE }, // L18 - { 6, 2, 2, 1, 50, 8, 2, TRUE }, // L19 MINI-BOSS - { 4, 2, 2, 1, 125, 8, 2, TRUE }, // L20 BOSS 5 FINAL -}; +static void assignGunnerPatterns(u8 lvl){ + u8 pat; + if(lvl < 3) pat = 0; // Cycle 1: radial burst + else if(lvl < 6) pat = 1; // Cycle 2: aimed fan + 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){ for(u8 i = 0; i < count; i++){ - u8 zone = i % 4; + u8 zone = i % SECTION_COUNT; 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){ if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1; level = lvl; - const struct LevelDef* def = &levels[lvl]; + grazeCount = 0; - distributeEnemies(ENEMY_TYPE_DRONE, def->drones); - distributeEnemies(ENEMY_TYPE_GUNNER, def->gunners); - distributeEnemies(ENEMY_TYPE_HUNTER, def->hunters); - distributeEnemies(ENEMY_TYPE_BUILDER, def->builders); + // Generate enemy composition + u8 counts[TP_POOL_SIZE]; + generateLevel(lvl, counts); - // set gunner pattern based on level def - for(s16 i = 0; i < ENEMY_COUNT; i++){ - if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){ - if(def->gunnerPattern == 2) - enemies[i].ints[0] = random() % 2; - else - enemies[i].ints[0] = def->gunnerPattern; - } + // Spawn enemies by type + for(u8 i = 0; i < TP_POOL_SIZE; i++){ + if(counts[i] > 0) + distributeEnemies(poolTypeMap[i], counts[i]); } - if(def->bossHp > 0){ - pendingBossHp = def->bossHp; - pendingBossNum = lvl / 4; // L3=0, L7=1, L11=2, L15=3, L18+=4 + // Assign gunner patterns + assignGunnerPatterns(lvl); + + // Boss spawn + if(isBossLevel(lvl)){ + pendingBossNum = lvl / 3; if(pendingBossNum > 4) pendingBossNum = 4; - if(lvl == 18) pendingBossNum = 1; // L19 mini-boss reuses boss 2 + pendingBossHp = bossHpTable[pendingBossNum]; spawnEnemy(ENEMY_TYPE_BOSS, 1); } - // spawn treasures - u8 treasureToSpawn = def->treasures; - for(u8 zone = 0; zone < 4 && treasureToSpawn > 0; zone++){ + // Spawn treasures + u8 treasureToSpawn = getTreasureCount(lvl); + for(u8 zone = 0; zone < SECTION_COUNT && treasureToSpawn > 0; zone++){ u8 perZone = treasureToSpawn >= 4 ? 2 : 1; for(u8 h = 0; h < perZone && treasureToSpawn > 0; h++){ spawnTreasure(zone); @@ -178,6 +193,7 @@ void loadLevel(u8 lvl){ } } + loadBgPalette(lvl % 3); loadMap(); } diff --git a/src/starfield.h b/src/starfield.h new file mode 100644 index 0000000..58a847e --- /dev/null +++ b/src/starfield.h @@ -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.3–1.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; + } +} diff --git a/src/start.h b/src/start.h index 13ce875..57dd91d 100644 --- a/src/start.h +++ b/src/start.h @@ -5,40 +5,75 @@ s16 startClock; -static void updateTransition(s16 startTime, bool last){ - if(startClock >= startTime && startClock < startTime + TRANS_TIME){ - 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(){ + VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 12, 6, 0, DMA); } -static void drawStartSplash(){ - 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); -} +s16 startScroll; static void drawStartBg(){ 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(){ - // VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 16 + 256), 26, 2, 0, DMA); - VDP_drawText(" PRESS ANY", 19, 18); - VDP_drawText(" BUTTON", 19, 19); - VDP_drawText(" T. BODDY", 19, 24); - VDP_drawText(" 02.2026", 19, 25); + drawStartMenuTop(); + drawStartMenuMid(); + drawStartMenuBottom(); } static void loadGameFromStart(){ @@ -49,12 +84,122 @@ static void loadGameFromStart(){ loadGame(); } -s16 startTime; -static void updateStartMenu(){ - if(startTime == 0 && (ctrl.start || ctrl.a || ctrl.b || ctrl.c)){ - XGM2_stop(); - startTime = 30; +static void loadAttractFromStart(){ + isAttract = TRUE; + attractClock = ATTRACT_DURATION; + loadGameFromStart(); +} + +#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){ startTime--; 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(){ - VDP_loadTileSet(startFade1.tileset, START_I, DMA); - VDP_loadTileSet(startFade2.tileset, START_I + 1, DMA); - VDP_loadTileSet(startFade3.tileset, START_I + 2, DMA); - VDP_loadTileSet(startFade4.tileset, START_I + 3, DMA); + static const u16 palBlack[64] = {0}; + PAL_setColors(0, palBlack, 64, CPU); + getHighScore(); + 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(); - XGM2_play(bgmStart); } void updateStart(){ - updateTransition(0, FALSE); - updateTransition(TRANS_TIME, TRUE); - if(startClock == TRANS_TIME) drawStartBg(); - else if(startClock == TRANS_TIME + 40) drawStartMenu(); - else if(startClock > TRANS_TIME + 40) updateStartMenu(); + if(startScroll < 0 && startClock >= TRANS_TIME){ + startScroll += 8; + // if(startScroll > 0) startScroll = 0; + VDP_setVerticalScroll(BG_A, startScroll); + } + + 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++; } diff --git a/src/treasure.h b/src/treasure.h index f737a3b..02b9cf7 100644 --- a/src/treasure.h +++ b/src/treasure.h @@ -19,12 +19,13 @@ void spawnTreasure(u8 zone){ treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1); 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)); if(!treasures[i].image){ treasures[i].active = FALSE; return; } + SPR_setDepth(treasures[i].image, 2); SPR_setVisibility(treasures[i].image, HIDDEN); treasures[i].type = random() % 4; SPR_setAnim(treasures[i].image, treasures[i].type); @@ -34,8 +35,13 @@ static void updateTreasure(u8 i){ switch(treasures[i].state){ case TREASURE_WALKING: // 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)) - treasures[i].vel.y *= -1; + if(treasures[i].pos.y >= GAME_H_F - FIX32(16)){ + 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 if(treasures[i].pos.x >= GAME_WRAP) @@ -43,8 +49,8 @@ static void updateTreasure(u8 i){ if(treasures[i].pos.x < 0) treasures[i].pos.x += GAME_WRAP; - treasures[i].pos.x += treasures[i].vel.x; - treasures[i].pos.y += treasures[i].vel.y; + treasures[i].pos.x += treasures[i].vel.x - (player.vel.x >> 2); + treasures[i].pos.y += treasures[i].vel.y - (playerScrollVelY >> 3); break; case TREASURE_CARRIED: @@ -109,8 +115,20 @@ static void updateTreasure(u8 i){ if(treasures[i].state != TREASURE_CARRIED && treasures[i].state != TREASURE_COLLECTED){ fix32 dy = treasures[i].pos.y - player.pos.y; if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){ - score += (treasures[i].state == TREASURE_FALLING) ? 2000 : 1000; - sfxPickup(); + score += (treasures[i].state == TREASURE_FALLING) ? 4096 : 1024; + // 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; treasureCollectedClock = 120; // 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 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); if(visible && treasures[i].state == TREASURE_COLLECTED){ if(player.respawnClock > 0) visible = FALSE; - else if(player.recoveringClock > 0) + else if(player.recoveringClock > 0 && player.recoverFlash) visible = (player.recoveringClock % 20 > 10); } SPR_setVisibility(treasures[i].image, visible ? VISIBLE : HIDDEN);