From 073f96c9b1d82ac04fdb42b37c121d39481d9bbb Mon Sep 17 00:00:00 2001 From: Trevor Boddy Date: Wed, 15 Apr 2026 08:19:29 -0400 Subject: [PATCH] pickups, native build, enemy/bullet/stage overhaul - Add pickup system (bomb, spread, rapid, shield) with new sprites - Replace Docker build with native SGDK compile via m68k-elf-gcc - Rework enemy spawning, homing math, boss HP/number globals - Expand chrome: score popups, minimap, pause/game over improvements - Overhaul stage generation with threat-point system - Add explosion sprites, shield sprite, powerup sprite - Add tools/ for sprite downscaling utilities --- .gitignore | 3 +- build.sh | 13 +- compile.sh | 60 +- res/explosions.png | Bin 0 -> 7075 bytes res/explosionsbig.png | Bin 0 -> 4749 bytes res/powerup.png | Bin 0 -> 4479 bytes res/resources.res | 5 +- res/shield.png | Bin 0 -> 3794 bytes run.sh | 4 +- runblastem.sh | 1 - src/background.h | 42 +- src/bonus.h | 21 +- src/bullets.h | 93 +-- src/chrome.h | 132 ++- src/enemies.h | 264 ++++-- src/enemytypes.h | 1617 +++++++++++++++++-------------------- src/global.h | 278 +++++-- src/main.c | 39 +- src/pickup.h | 154 ++++ src/player.h | 101 ++- src/stage.h | 135 +--- src/treasure.h | 4 +- tools/downscale_sprite.py | 540 +++++++++++++ tools/suika.png | Bin 0 -> 5610 bytes tools/suikasm.png | Bin 0 -> 2442 bytes 25 files changed, 2320 insertions(+), 1186 deletions(-) create mode 100644 res/explosions.png create mode 100644 res/explosionsbig.png create mode 100644 res/powerup.png create mode 100644 res/shield.png delete mode 100755 runblastem.sh create mode 100644 src/pickup.h create mode 100644 tools/downscale_sprite.py create mode 100644 tools/suika.png create mode 100644 tools/suikasm.png diff --git a/.gitignore b/.gitignore index 8c4096a..380a9ca 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ dist/ **/**.psd *.pdf **/**.pdf -docs/ \ No newline at end of file +docs/ +sgdk/ \ No newline at end of file diff --git a/build.sh b/build.sh index faabf93..a847451 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,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.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 +#!/bin/bash +set -e + +# Build and launch emulator +./compile.sh +/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin diff --git a/compile.sh b/compile.sh index fada6ac..57cabaa 100755 --- a/compile.sh +++ b/compile.sh @@ -1,2 +1,58 @@ -rm -rf res/resources.o res/resources.h out/* -docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.11 \ No newline at end of file +#!/bin/bash +set -e + +# Native SGDK compile script (no Docker) +# Requires: brew install m68k-elf-gcc, java + +GDK=sgdk +BIN=$GDK/bin +LIB=$GDK/lib +INC=$GDK/inc +RES_LIB=$GDK/res +SRC_LIB=$GDK/src + +CC=m68k-elf-gcc +OBJCPY=m68k-elf-objcopy +NM=m68k-elf-nm +JAVA=java +RESCOMP="$JAVA -jar $BIN/rescomp.jar" +SIZEBND="$JAVA -jar $BIN/sizebnd.jar" + +INCS="-I. -Iinc -Isrc -Ires -Iout -I$INC -I$RES_LIB" +FLAGS="-DSGDK_GCC -m68000 -Wall -Wextra -Wno-shift-negative-value -Wno-main -Wno-unused-parameter -Wno-implicit-function-declaration -fno-builtin -ffunction-sections -fdata-sections -fms-extensions $INCS -B$BIN" +RELEASE_FLAGS="$FLAGS -O3 -fuse-linker-plugin -fno-web -fno-gcse -fomit-frame-pointer -flto -flto=auto -ffat-lto-objects" + +# Clean +rm -rf res/resources.rs res/resources.h res/resources.o out/* +mkdir -p out + +echo "==> Compiling resources..." +$RESCOMP res/resources.res out/resources.rs -dep out/resources.d + +echo "==> Compiling rom_head..." +$CC $FLAGS -c src/boot/rom_head.c -o out/rom_head.o +$OBJCPY -O binary out/rom_head.o out/rom_head.bin + +echo "==> Compiling sega.s..." +$CC -x assembler-with-cpp -Wa,--register-prefix-optional,--bitwise-or $RELEASE_FLAGS -c src/boot/sega.s -o out/sega.o + +echo "==> Compiling resources assembly..." +$CC -x assembler-with-cpp -Wa,--register-prefix-optional,--bitwise-or $RELEASE_FLAGS -c out/resources.rs -o out/resources.o + +echo "==> Compiling main.c..." +$CC $RELEASE_FLAGS -c src/main.c -o out/main.o + +echo "==> Linking..." +$CC -m68000 -B$BIN -n -T $GDK/md.ld -nostdlib out/sega.o out/resources.o out/main.o $LIB/libmd.a -lgcc -o out/rom.out -Wl,--gc-sections -flto -flto=auto -ffat-lto-objects + +echo "==> Extracting ROM..." +$OBJCPY -O binary out/rom.out out/rom.bin + +echo "==> Generating symbol table..." +$NM -n -l out/rom.out > out/symbol.txt + +echo "==> Padding ROM..." +$SIZEBND out/rom.bin -sizealign 131072 -checksum + +echo "==> Done! out/rom.bin" +ls -la out/rom.bin diff --git a/res/explosions.png b/res/explosions.png new file mode 100644 index 0000000000000000000000000000000000000000..fc52f1c9931f829a8f793d2d16f6e2d0e57f5a50 GIT binary patch literal 7075 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 CiYWE~ literal 0 HcmV?d00001 diff --git a/res/explosionsbig.png b/res/explosionsbig.png new file mode 100644 index 0000000000000000000000000000000000000000..0758a4c509cd46b5d496e239c151f2061e7f8288 GIT binary patch literal 4749 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 literal 0 HcmV?d00001 diff --git a/run.sh b/run.sh index a96a093..8cf829a 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,2 @@ -#/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 +/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/runblastem.sh b/runblastem.sh deleted file mode 100755 index f414170..0000000 --- a/runblastem.sh +++ /dev/null @@ -1 +0,0 @@ -./blastem/blastem out/rom.bin \ No newline at end of file diff --git a/src/background.h b/src/background.h index d5b4db5..d3b58a7 100644 --- a/src/background.h +++ b/src/background.h @@ -26,7 +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); + // VDP_loadTileSet(door.tileset, BG_I + 256, DMA); // for(u8 y = 0; y < 14; y++){ // for(u8 x = 0; x < 64; x++){ @@ -53,13 +53,13 @@ void loadBackground(){ } } - // 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; - } + // // 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] = F32_mul(player.camera + FIX32(256), parallaxMul[i]); @@ -97,19 +97,19 @@ void updateBackground(){ VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); - // 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; - } - } + // // 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 diff --git a/src/bonus.h b/src/bonus.h index 5cd46c0..53b930d 100644 --- a/src/bonus.h +++ b/src/bonus.h @@ -211,6 +211,9 @@ static void updateBonusObj(u8 i){ if(distSq < collDist * collDist){ if(obj->isBomb){ sfxPlayerHit(); + sfxExplosion(); + spawnExplosion(player.camera + FIX32(bonusCursorX), FIX32(bonusCursorY), 3, TRUE); + XGM2_stop(); bonusExiting = TRUE; bonusExitClock = 0; if(bonusCursor) SPR_setVisibility(bonusCursor, HIDDEN); @@ -305,7 +308,7 @@ void loadBonus(u8 variant){ drawBonusStars(); #if MUSIC_VOLUME > 0 - XGM2_play(treasureMusic); + XGM2_play(bossMusic); #endif } @@ -348,6 +351,10 @@ void updateBonus(){ bonusCl++; + // Clear "Entering Bonus Level" text after intro + if(bonusCl == 90) + VDP_clearText(10, 14, 20); + // Alternate bg variant every 60 frames if(starAlternate && bonusCl % 30 == 0) starVariant ^= 1; @@ -365,6 +372,9 @@ void updateBonus(){ VDP_drawText("Bonus", 14, 16); VDP_drawText(bonusStr, 20, 16); sfxCollectAllTreasures(); +#if MUSIC_VOLUME > 0 + XGM2_play(treasureMusic); +#endif } if(bonusExitClock == 220) PAL_fadeOut(0, 31, 20, TRUE); @@ -381,8 +391,11 @@ void updateBonus(){ fix16 diff = targetAngle - bonusAngle; if(diff > FIX16(180)) diff -= FIX16(360); if(diff < FIX16(-180)) diff += FIX16(360); - // Determine target angular velocity from shortest-path direction - fix16 targetVel = (diff > 0) ? BONUS_ANGLE_SPEED : -BONUS_ANGLE_SPEED; + // Determine target angular velocity — proportional when close to prevent overshoot oscillation + fix16 targetVel; + if(diff > BONUS_ANGLE_SPEED) targetVel = BONUS_ANGLE_SPEED; + else if(diff < -BONUS_ANGLE_SPEED) targetVel = -BONUS_ANGLE_SPEED; + else targetVel = diff; // Accelerate toward target velocity if(bonusAngleVel < targetVel){ bonusAngleVel += BONUS_ANGLE_ACCEL; @@ -462,6 +475,8 @@ void clearBonus(){ } bonusObjs[i].active = FALSE; } + // Clear any active explosions + clearExplosions(); // Clear starfield tiles from BG_B clearStarfield(); // Clear bonus text from BG_A diff --git a/src/bullets.h b/src/bullets.h index 1e1a831..0e69339 100644 --- a/src/bullets.h +++ b/src/bullets.h @@ -63,9 +63,8 @@ bool spawnBullet(struct bulletSpawner spawner, void(*updater)){ bullets[i].vel.y = F32_mul(F32_sin(spawner.angle), spawner.speed); } bullets[i].updater = updater; - bullets[i].explosion = FALSE; bullets[i].grazed = FALSE; - bullets[i].dist = bullets[i].player ? 24 : (spawner.anim == 0 ? 4 : 7); + bullets[i].dist = bullets[i].player ? 32 : (spawner.anim == 0 ? 4 : 7); // zero out ints array for(s16 j = 0; j < PROP_COUNT; j++) bullets[i].ints[j] = spawner.ints[j]; @@ -103,8 +102,10 @@ static void collideWithEnemy(u8 i){ deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){ bulletDist = getApproximatedDistance(F32_toInt(deltaX), F32_toInt(deltaY)); if(bulletDist <= bullets[i].dist){ - score += (enemies[j].ints[3] >= 0) ? 512 : 256; - killBullet(i, TRUE); + u32 pts = (enemies[j].carriedTreasure >= 0) ? 512 : 256; + score += pts; + spawnPopup(enemies[j].pos.x, enemies[j].pos.y, pts); + killBullet(i, enemies[j].hp > 1); killEnemy(j); sfxExplosion(); } @@ -122,44 +123,42 @@ static void collideWithPlayer(u8 i){ F32_toInt(deltaX), F32_toInt(deltaY)); if(dist <= 4){ - // kill enemy bullet, then spawn a fresh player bullet explosion + // kill enemy bullet, then spawn big explosion at player position + u8 expAnim = getBulletExplosionAnim(i); 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; - } - } + spawnExplosion(player.pos.x, player.pos.y, expAnim, TRUE); sfxExplosion(); if(!isAttract){ - player.lives--; - if(player.lives == 0){ - gameOver = TRUE; - XGM2_stop(); - sfxGameOver(); - } else { - sfxPlayerHit(); - player.respawnClock = 120; - SPR_setVisibility(player.image, HIDDEN); + if(player.hasShield){ + // shield absorbs hit + player.hasShield = FALSE; + player.shieldClock = 0; + removeShieldVisual(); + player.recoveringClock = 60; + player.recoverFlash = TRUE; killBullets = TRUE; hitMessageClock = 120; hitMessageBullet = TRUE; + } else { + player.lives--; + if(player.lives == 0){ + gameOver = TRUE; + XGM2_stop(); + sfxGameOver(); + } else { + sfxPlayerHit(); + levelPerfect = FALSE; + player.respawnClock = 120; + player.activePowerup = 0; + player.powerupClock = 0; + player.hasShield = FALSE; + player.shieldClock = 0; + removeShieldVisual(); + SPR_setVisibility(player.image, HIDDEN); + killBullets = TRUE; + hitMessageClock = 120; + hitMessageBullet = TRUE; + } } } } else if(dist <= GRAZE_RADIUS && !bullets[i].grazed){ @@ -170,27 +169,7 @@ static void collideWithPlayer(u8 i){ } } -static void updateBulletExplosion(u8 i){ - bullets[i].clock++; - if(bullets[i].clock & 1){ - bullets[i].frame++; - if(bullets[i].frame >= 5){ - killBullet(i, FALSE); - return; - } - SPR_setFrame(bullets[i].image, bullets[i].frame); - } - s16 sx = getScreenX(bullets[i].pos.x, player.camera); - s16 sy = F32_toInt(bullets[i].pos.y); - u8 off = BULLET_OFF; - SPR_setPosition(bullets[i].image, sx - off, sy - off); -} - static void updateBullet(u8 i){ - if(bullets[i].explosion){ - updateBulletExplosion(i); - return; - } bullets[i].pos.x += bullets[i].vel.x - (player.vel.x >> 3); bullets[i].pos.y += bullets[i].vel.y - (playerScrollVelY >> 3); @@ -234,7 +213,7 @@ void updateBullets(){ if(killBullets){ killBullets = FALSE; for(s16 i = 0; i < BULLET_COUNT; i++) - if(bullets[i].active && !bullets[i].player && !bullets[i].explosion) killBullet(i, TRUE); + if(bullets[i].active && !bullets[i].player) 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 43c3a8c..0f7eb45 100644 --- a/src/chrome.h +++ b/src/chrome.h @@ -8,6 +8,66 @@ u16 hudPal = PAL0; +#define POPUP_COUNT 4 + +struct scorePopup { + bool active; + u8 clock; + u8 len; + s16 tileX, tileY; + char text[6]; +}; +struct scorePopup popups[POPUP_COUNT]; + +void spawnPopup(fix32 worldX, fix32 worldY, u32 value){ + s16 slot = -1; + for(s16 i = 0; i < POPUP_COUNT; i++) if(!popups[i].active){ slot = i; break; } + if(slot == -1) return; + s16 screenX = getScreenX(worldX, player.camera); + s16 screenY = F32_toInt(worldY); + s16 tX = screenX / 8; + s16 tY = screenY / 8; + tX--; + if(tX < 0) tX = 0; + if(tX > 38) tX = 38; + if(tY < 6) tY = 6; + if(tY > 25) tY = 25; + popups[slot].tileX = tX; + popups[slot].tileY = tY; + uintToStr(value, popups[slot].text, 1); + popups[slot].len = strlen(popups[slot].text); + popups[slot].clock = 0; + popups[slot].active = TRUE; + bigText(popups[slot].text, tX, tY, TRUE); +} + +static void updatePopups(){ + for(s16 i = 0; i < POPUP_COUNT; i++){ + if(!popups[i].active) continue; + popups[i].clock++; + if(popups[i].clock >= 24){ + VDP_clearTileMapRect(BG_A, popups[i].tileX, popups[i].tileY, popups[i].len, 2); + popups[i].active = FALSE; + continue; + } + if(popups[i].clock % 8 == 0){ + VDP_clearTileMapRect(BG_A, popups[i].tileX, popups[i].tileY, popups[i].len, 2); + popups[i].tileY--; + if(popups[i].tileY < 6) popups[i].tileY = 6; + bigText(popups[i].text, popups[i].tileX, popups[i].tileY, TRUE); + } + } +} + +void clearPopups(){ + for(s16 i = 0; i < POPUP_COUNT; i++){ + if(popups[i].active){ + VDP_clearTileMapRect(BG_A, popups[i].tileX, popups[i].tileY, popups[i].len, 2); + popups[i].active = FALSE; + } + } +} + #define FONT_BIG_I 340 void bigText(char* str, u16 x, u16 y, bool shadow){ @@ -185,6 +245,10 @@ static void updateMap(){ VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow); } +// pickup HUD tracking +s16 lastBombCount = -1; +s16 lastPowerupState = -1; // 0=none, 1=spread, 2=rapid, 3=shield (composite) + u8 phraseIndex[4]; s16 lastLevel; @@ -221,11 +285,42 @@ static void repaintMap(){ VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + mapPlayerRow); } +static void drawBombCount(){ + if(isAttract) return; + if(player.bombCount > 0){ + char bStr[4] = "B:"; + char numStr[2]; + uintToStr(player.bombCount, numStr, 1); + bStr[2] = numStr[0]; + bStr[3] = 0; + VDP_drawText(bStr, 1, 7); + } else { + VDP_clearText(1, 7, 3); + } + lastBombCount = player.bombCount; +} + +static void drawPowerupIndicator(){ + if(isAttract) return; + VDP_clearText(1, 8, 6); + if(player.hasShield) + VDP_drawText("SH", 1, 8); + else if(player.activePowerup == 1) + VDP_drawText("SPREAD", 1, 8); + else if(player.activePowerup == 2) + VDP_drawText("RAPID", 1, 8); + s16 state = player.activePowerup; + if(player.hasShield) state = 3; + lastPowerupState = state; +} + static void repaintHud(){ bigText(scoreStr, SCORE_X, SCORE_Y, FALSE); drawLives(); repaintMap(); drawLevel(); + drawBombCount(); + drawPowerupIndicator(); } void loadChrome(){ @@ -263,7 +358,7 @@ static void doGameOver(){ void noop(s16 j){ (void)j; } spawnBullet(spawner, noop); for(s16 j = BULLET_COUNT - 1; j >= 0; j--){ - if(bullets[j].active && !bullets[j].explosion + if(bullets[j].active && bullets[j].pos.x == treasures[i].pos.x && bullets[j].pos.y == treasures[i].pos.y){ killBullet(j, TRUE); break; @@ -273,6 +368,7 @@ static void doGameOver(){ killTreasure(i); } SPR_releaseSprite(player.image); + clearPickups(); // clear lives VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16); @@ -297,6 +393,7 @@ 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); + for(s16 i = 0; i < PICKUP_COUNT; i++) if(pickups[i].active) SPR_setPalette(pickups[i].image, PAL1); SPR_setPalette(player.image, PAL1); hudPal = PAL1; hudPal = PAL1; @@ -309,6 +406,7 @@ static void clearPause(){ for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL0); for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0); for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0); + for(s16 i = 0; i < PICKUP_COUNT; i++) if(pickups[i].active) SPR_setPalette(pickups[i].image, PAL0); SPR_setPalette(player.image, PAL0); hudPal = PAL0; repaintHud(); @@ -345,12 +443,13 @@ static void updatePause(){ } #define TRANSITION_TREASURE_X 10 -#define TRANSITION_TREASURE_Y 13 +#define TRANSITION_TREASURE_Y 15 #define TRANSITION_LEVEL_X 12 -#define TRANSITION_LEVEL_Y 15 +#define TRANSITION_LEVEL_Y 13 void updateChrome(){ + updatePopups(); updatePause(); if(gameOver && !didGameOver) doGameOver(); if(didGameOver){ @@ -396,12 +495,31 @@ void updateChrome(){ else VDP_drawText("Lives Left", TRANSITION_LEVEL_X + 2, TRANSITION_LEVEL_Y + 5); + if(grazeCount > 0){ + char grazeStr[8]; + char grazePtsStr[12]; + uintToStr(grazeCount, grazeStr, 1); + uintToStr(grazeCount * 64, grazePtsStr, 1); + VDP_drawText("Grazes", TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y + 7); + VDP_drawText(grazeStr, TRANSITION_LEVEL_X + 7, TRANSITION_LEVEL_Y + 7); + VDP_drawText(grazePtsStr, TRANSITION_LEVEL_X + 7 + strlen(grazeStr) + 1, TRANSITION_LEVEL_Y + 7); + VDP_drawText("pts", TRANSITION_LEVEL_X + 7 + strlen(grazeStr) + 1 + strlen(grazePtsStr) + 1, TRANSITION_LEVEL_Y + 7); + } + + if(levelPerfect){ + score += 4096; + lastScore = score; + VDP_drawText("PERFECT! +4096", 13, TRANSITION_LEVEL_Y + 9); + } + } 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); + VDP_clearText(0, TRANSITION_LEVEL_Y + 7, 40); + VDP_clearText(0, TRANSITION_LEVEL_Y + 9, 40); } return; } @@ -430,6 +548,7 @@ void updateChrome(){ } if(allDone && collectedCount > 0){ allTreasureCollected = TRUE; + score += 4096; 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"}; @@ -466,5 +585,12 @@ void updateChrome(){ allTreasureCollected = FALSE; VDP_drawText("All Enemies Down!", 12, 5); } + // pickup HUD + if(!isAttract){ + if(lastBombCount != player.bombCount) drawBombCount(); + s16 curPowerup = player.activePowerup; + if(player.hasShield) curPowerup = 3; + if(lastPowerupState != curPowerup) drawPowerupIndicator(); + } if(clock % 4 == 0) updateMap(); } \ No newline at end of file diff --git a/src/enemies.h b/src/enemies.h index ffb15b0..ed99058 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -42,11 +42,13 @@ void spawnEnemy(u8 type, u8 zone){ enemies[i].pos.x = randX; enemies[i].pos.y = randY; + + // Default sprite — load functions can override via SPR_setDefinition() 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]; + 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)); @@ -61,23 +63,62 @@ void spawnEnemy(u8 type, u8 zone){ enemies[i].ints[j] = 0; enemies[i].fixes[j] = 0; } - enemies[i].ints[3] = -1; + enemies[i].canGrabTreasure = FALSE; + enemies[i].homesOnPlayer = FALSE; + enemies[i].canShoot = FALSE; + enemies[i].canFlipH = FALSE; + enemies[i].useBigSprite = FALSE; + enemies[i].carriedTreasure = -1; + enemies[i].targetTreasure = -1; enemies[i].anim = 0; switch(enemies[i].type){ - case ENEMY_TYPE_TEST: + case ENEMY_TYPE_ONE: loadEnemyOne(i); break; - case ENEMY_TYPE_DRONE: - loadDrone(i); + case ENEMY_TYPE_TWO: + loadEnemyTwo(i); break; - case ENEMY_TYPE_GUNNER: - loadGunner(i); + case ENEMY_TYPE_THREE: + loadEnemyThree(i); break; - case ENEMY_TYPE_HUNTER: - loadHunter(i); + case ENEMY_TYPE_FOUR: + loadEnemyFour(i); break; - case ENEMY_TYPE_BUILDER: - loadBuilder(i); + case ENEMY_TYPE_FIVE: + loadEnemyFive(i); + break; + case ENEMY_TYPE_SIX: + loadEnemySix(i); + break; + case ENEMY_TYPE_SEVEN: + loadEnemySeven(i); + break; + case ENEMY_TYPE_EIGHT: + loadEnemyEight(i); + break; + case ENEMY_TYPE_NINE: + loadEnemyNine(i); + break; + case ENEMY_TYPE_TEN: + loadEnemyTen(i); + break; + case ENEMY_TYPE_ELEVEN: + loadEnemyEleven(i); + break; + case ENEMY_TYPE_TWELVE: + loadEnemyTwelve(i); + break; + case ENEMY_TYPE_THIRTEEN: + loadEnemyThirteen(i); + break; + case ENEMY_TYPE_FOURTEEN: + loadEnemyFourteen(i); + break; + case ENEMY_TYPE_FIFTEEN: + loadEnemyFifteen(i); + break; + case ENEMY_TYPE_SIXTEEN: + loadEnemySixteen(i); break; case ENEMY_TYPE_BOSS: loadBoss(i); @@ -89,11 +130,11 @@ void spawnEnemy(u8 type, u8 zone){ } static void boundsEnemy(u8 i){ - if((enemies[i].type == ENEMY_TYPE_TEST || enemies[i].type == ENEMY_TYPE_BUILDER) && enemies[i].ints[3] >= 0){ - s16 h = enemies[i].ints[3]; + if(enemies[i].canGrabTreasure && enemies[i].carriedTreasure >= 0){ + s16 h = enemies[i].carriedTreasure; // if the treasure was collected by player or gone, kill this enemy if(!treasures[h].active || treasures[h].state == TREASURE_COLLECTED){ - enemies[i].ints[3] = -1; + enemies[i].carriedTreasure = -1; treasureBeingCarried = FALSE; killEnemy(i); return; @@ -108,17 +149,13 @@ static void boundsEnemy(u8 i){ treasures[h].vel.x = 0; treasures[h].vel.y = FIX32(3); } - enemies[i].ints[3] = -1; + enemies[i].carriedTreasure = -1; treasureBeingCarried = FALSE; enemies[i].vel.y = FIX32(1); } else { if(treasures[h].active) killTreasure(h); - enemies[i].ints[3] = -1; + enemies[i].carriedTreasure = -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); } @@ -143,6 +180,64 @@ static void boundsEnemy(u8 i){ } } +static void enemySeekTreasure(u8 i){ + // carrying: steer upward + if(enemies[i].carriedTreasure >= 0){ + 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; + } + + // cancel target if a treasure is already being carried + if(treasureBeingCarried && enemies[i].targetTreasure >= 0){ + enemies[i].targetTreasure = -1; + } + + // scan for nearest walking treasure every 30 frames + if(!treasureBeingCarried && enemies[i].clock % 30 == 0){ + s16 bestTreasure = -1; + fix32 bestDist = FIX32(9999); + for(s16 j = 0; j < TREASURE_COUNT; j++){ + if(!treasures[j].active || treasures[j].state != TREASURE_WALKING) continue; + fix32 dx = getWrappedDelta(enemies[i].pos.x, treasures[j].pos.x); + fix32 dy = enemies[i].pos.y - treasures[j].pos.y; + fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy); + if(dist < bestDist && dist < FIX32(256)){ + bestDist = dist; + bestTreasure = j; + } + } + enemies[i].targetTreasure = bestTreasure; + } + + // steer toward target treasure + if(enemies[i].targetTreasure >= 0){ + s16 t = enemies[i].targetTreasure; + if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){ + enemies[i].targetTreasure = -1; + } else { + fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x); + fix32 dy = treasures[t].pos.y - enemies[i].pos.y; + + 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; + fix32 ady = dy < 0 ? -dy : dy; + if(adx < FIX32(16) && ady < FIX32(16)){ + enemies[i].carriedTreasure = t; + enemies[i].targetTreasure = -1; + treasureBeingCarried = TRUE; + treasures[t].state = TREASURE_CARRIED; + treasures[t].carriedBy = i; + } + } + } +} + static void updateEnemy(u8 i){ enemies[i].pos.x += enemies[i].vel.x - (player.vel.x >> 3); enemies[i].pos.y += enemies[i].vel.y - (playerScrollVelY >> 3); @@ -153,21 +248,67 @@ static void updateEnemy(u8 i){ fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x); enemies[i].onScreen = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); + // flag-based treasure seeking (before type-specific update) + if(enemies[i].canGrabTreasure){ + enemySeekTreasure(i); + } + + // flag-based homing + if(enemies[i].homesOnPlayer){ + 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); + } + switch(enemies[i].type){ - case ENEMY_TYPE_TEST: + case ENEMY_TYPE_ONE: updateEnemyOne(i); break; - case ENEMY_TYPE_DRONE: - updateDrone(i); + case ENEMY_TYPE_TWO: + updateEnemyTwo(i); break; - case ENEMY_TYPE_GUNNER: - updateGunner(i); + case ENEMY_TYPE_THREE: + updateEnemyThree(i); break; - case ENEMY_TYPE_HUNTER: - updateHunter(i); + case ENEMY_TYPE_FOUR: + updateEnemyFour(i); break; - case ENEMY_TYPE_BUILDER: - updateBuilder(i); + case ENEMY_TYPE_FIVE: + updateEnemyFive(i); + break; + case ENEMY_TYPE_SIX: + updateEnemySix(i); + break; + case ENEMY_TYPE_SEVEN: + updateEnemySeven(i); + break; + case ENEMY_TYPE_EIGHT: + updateEnemyEight(i); + break; + case ENEMY_TYPE_NINE: + updateEnemyNine(i); + break; + case ENEMY_TYPE_TEN: + updateEnemyTen(i); + break; + case ENEMY_TYPE_ELEVEN: + updateEnemyEleven(i); + break; + case ENEMY_TYPE_TWELVE: + updateEnemyTwelve(i); + break; + case ENEMY_TYPE_THIRTEEN: + updateEnemyThirteen(i); + break; + case ENEMY_TYPE_FOURTEEN: + updateEnemyFourteen(i); + break; + case ENEMY_TYPE_FIFTEEN: + updateEnemyFifteen(i); + break; + case ENEMY_TYPE_SIXTEEN: + updateEnemySixteen(i); break; case ENEMY_TYPE_BOSS: updateBoss(i); @@ -180,46 +321,43 @@ static void updateEnemy(u8 i){ fix32 edy = enemies[i].pos.y - player.pos.y; if(edx >= FIX32(-16) && edx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){ sfxExplosion(); - // spawn explosion at player position - 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; - } - } + // spawn big explosion at player position + spawnExplosion(player.pos.x, player.pos.y, 3, TRUE); // yellow if(enemies[i].type != ENEMY_TYPE_BOSS){ enemies[i].hp = 0; killEnemy(i); } if(!isAttract){ - player.lives--; - if(player.lives == 0){ - gameOver = TRUE; - XGM2_stop(); - sfxGameOver(); - } else { - sfxPlayerHit(); - player.respawnClock = 120; - SPR_setVisibility(player.image, HIDDEN); + if(player.hasShield){ + // shield absorbs hit + player.hasShield = FALSE; + player.shieldClock = 0; + removeShieldVisual(); + player.recoveringClock = 60; + player.recoverFlash = TRUE; killBullets = TRUE; hitMessageClock = 120; hitMessageBullet = FALSE; + } else { + player.lives--; + if(player.lives == 0){ + gameOver = TRUE; + XGM2_stop(); + sfxGameOver(); + } else { + sfxPlayerHit(); + levelPerfect = FALSE; + player.respawnClock = 120; + player.activePowerup = 0; + player.powerupClock = 0; + player.hasShield = FALSE; + player.shieldClock = 0; + removeShieldVisual(); + SPR_setVisibility(player.image, HIDDEN); + killBullets = TRUE; + hitMessageClock = 120; + hitMessageBullet = FALSE; + } } } } @@ -228,7 +366,7 @@ static void updateEnemy(u8 i){ s16 sx = getScreenX(enemies[i].pos.x, player.camera); s16 sy = F32_toInt(enemies[i].pos.y); SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN); - if(enemies[i].type != ENEMY_TYPE_DRONE && enemies[i].type != ENEMY_TYPE_BOSS) + if(enemies[i].canFlipH) SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0); SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off); diff --git a/src/enemytypes.h b/src/enemytypes.h index 7c38add..28e485e 100644 --- a/src/enemytypes.h +++ b/src/enemytypes.h @@ -14,929 +14,511 @@ static fix16 enemyHoneAngle(u8 i){ 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); +static fix16 bulletHoneAngle(u8 i){ + fix32 dx = getWrappedDelta(player.pos.x, bullets[i].pos.x); + fix32 dy = player.pos.y - bullets[i].pos.y; + return getAngle(dx, dy); } -// 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(); -// } -// } +// stage 1 enemies -// 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].hp = 2; enemies[i].angle = FIX16((random() % 4) * 90 + 45); - enemies[i].speed = FIX32(2); + enemies[i].speed = FIX32(3); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 60; } void updateEnemyOne(u8 i){ - // carrying behavior: move upward, skip shooting - if(enemies[i].ints[3] >= 0){ - 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; - } + if(!enemies[i].onScreen || enemies[i].clock % 60 != enemies[i].ints[0]) return; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .speed = FIX32(5), + .angle = enemyHoneAngle(i) + }; + spawnBullet(spawner, EMPTY); +} - // cancel any target if a treasure is already being carried - if(treasureBeingCarried && enemies[i].ints[2] >= 0){ - enemies[i].ints[2] = -1; - } - - // seeking behavior: periodically look for a treasure to grab - if(!treasureBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){ - s16 bestTreasure = -1; - fix32 bestDist = FIX32(9999); - for(s16 j = 0; j < TREASURE_COUNT; j++){ - if(!treasures[j].active || treasures[j].state != TREASURE_WALKING) continue; - fix32 dx = getWrappedDelta(enemies[i].pos.x, treasures[j].pos.x); - fix32 dy = enemies[i].pos.y - treasures[j].pos.y; - fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy); - if(dist < bestDist && dist < FIX32(256)){ - bestDist = dist; - bestTreasure = j; - } - } - enemies[i].ints[2] = bestTreasure; - } - - // steer toward target treasure - if(enemies[i].ints[2] >= 0){ - s16 t = enemies[i].ints[2]; - if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){ - enemies[i].ints[2] = -1; - } else { - fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x); - fix32 dy = treasures[t].pos.y - enemies[i].pos.y; - - // hone toward treasure's current position at base 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; - fix32 ady = dy < 0 ? -dy : dy; - if(adx < FIX32(16) && ady < FIX32(16)){ - enemies[i].ints[3] = t; - enemies[i].ints[2] = -1; - treasureBeingCarried = TRUE; - treasures[t].state = TREASURE_CARRIED; - treasures[t].carriedBy = i; - return; - } - } - } - - // normal shooting - if((s16)(enemies[i].clock % 20) == enemies[i].ints[0] && enemies[i].onScreen){ - (s16)(enemies[i].clock % 40) == enemies[i].ints[0] ? sfxEnemyShotB() : sfxEnemyShotA(); +void loadEnemyTwo(u8 i){ + enemies[i].hp = 4; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(1); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 30; + enemies[i].ints[1] = enemies[i].ints[0] + 15; +} +void updateEnemyTwo(u8 i){ + if(enemies[i].clock % 60 == enemies[i].ints[0]) enemies[i].fixes[0] = enemyHoneAngle(i); + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 >= enemies[i].ints[0] && enemies[i].clock % 60 < enemies[i].ints[1] && enemies[i].clock % 5 == 0){ struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 3 + (random() % 3), - .speed = FIX32(4) + FIX32(random() % 4), - .angle = FIX16(random() % 45), - }; - switch(random() % 3){ - case 1: spawner.anim += 3; break; - case 2: spawner.anim += 6; break; - } - for(u8 j = 0; j < 8; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle = angleAdd(spawner.angle, FIX16(45)); - } - } -} - -// ============================================================================= -// --- Type 3: Hunter --- -// ============================================================================= -// Fast, relentless chaser. Homes toward the player EVERY frame with speed 5 -// (matching player's normal speed). Never shoots -- pure body-collision threat. -// Forces the player to keep moving and use diagonal movement to -// outrun. Very dangerous in groups. -// -// Behavior: -// - Recalculates angle toward player every frame (no delay like Drone). -// - Moves at speed 5 (player normal speed = 6). -// - Bounces off top/bottom screen edges. -// - No shooting, no abduction. Just chases. -// -// No custom ints used. -// -// Speed: 5 HP: 1 Shoots: no Abducts: no -// -// Design notes: -// - 2-3 hunters alongside gunners is very hard (dodge bullets while fleeing) -// - 4+ hunters with no gunners is a pure movement/positioning challenge -// - 6 hunters is near-impossible -- only for late-game punishment -// - Hunters are the anti-camping enemy: you can't sit still -// ============================================================================= -void loadHunter(u8 i){ - enemies[i].angle = FIX16(random() % 360); - enemies[i].speed = FIX32(5); -} -void updateHunter(u8 i){ - 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); -} - -// ============================================================================= -// --- Type 4: Builder (Abductor) --- -// ============================================================================= -// Treasure abductor. Drifts slowly, scans for walking treasures, grabs one, and -// flies upward. If it reaches the top of the screen with a treasure, the treasure -// is killed and a Gunner spawns at that position -- punishing the player for -// not intercepting. Only 1 treasure can be globally carried at a time -// (treasureBeingCarried flag). -// -// Behavior: -// - When NOT carrying: drifts at speed 0.7. Scans for nearest walking treasure -// within 256px every 30 frames. If a target is found, switches to seeking -// speed (1.4) and homes toward it. Grabs when within 16px. -// - When carrying: flies upward (angle 704-832) at speed 1.4. -// boundsEnemy() handles reaching the top: kills the treasure, spawns a Gunner -// at the builder's position, then self-destructs. -// - If the carried treasure gets collected by the player while being carried, -// boundsEnemy() detects this and kills the builder (enemy dies, treasure safe). -// - Cancels its target if another enemy is already carrying a treasure. -// - No shooting at all. -// -// ints[0] = random scan timer offset (0-59) -// ints[2] = target treasure index (-1 = no target) -// ints[3] = carried treasure index (-1 = not carrying) -// -// Speed: 0.7 (drift), 1.4 (seeking/carrying) HP: 1 Shoots: no -// Abducts: yes (kills treasure + spawns Gunner if it reaches the top) -// -// Design notes: -// - 1 builder adds light urgency. 2 builders is stressful. -// - Pairs well with drones to split the player's attention. -// - The Gunner spawn on success makes ignoring builders snowball badly. -// ============================================================================= -void loadBuilder(u8 i){ - enemies[i].ints[0] = random() % 60; - enemies[i].ints[2] = -1; - enemies[i].ints[3] = -1; - 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 = FIX16(248 + (random() % 45)); - enemies[i].speed = FIX32(1.4); - 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; - } - - // cancel target if a treasure is already being carried - if(treasureBeingCarried && enemies[i].ints[2] >= 0){ - enemies[i].ints[2] = -1; - } - - // scan for nearest walking treasure every 30 frames - if(!treasureBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){ - s16 bestTreasure = -1; - fix32 bestDist = FIX32(9999); - for(s16 j = 0; j < TREASURE_COUNT; j++){ - if(!treasures[j].active || treasures[j].state != TREASURE_WALKING) continue; - fix32 dx = getWrappedDelta(enemies[i].pos.x, treasures[j].pos.x); - fix32 dy = enemies[i].pos.y - treasures[j].pos.y; - fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy); - if(dist < bestDist && dist < FIX32(256)){ - bestDist = dist; - bestTreasure = j; - } - } - enemies[i].ints[2] = bestTreasure; - } - - // steer toward target treasure - if(enemies[i].ints[2] >= 0){ - s16 t = enemies[i].ints[2]; - if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){ - enemies[i].ints[2] = -1; - } else { - 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); - 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; - fix32 ady = dy < 0 ? -dy : dy; - if(adx < FIX32(16) && ady < FIX32(16)){ - enemies[i].ints[3] = t; - enemies[i].ints[2] = -1; - treasureBeingCarried = TRUE; - treasures[t].state = TREASURE_CARRIED; - treasures[t].carriedBy = i; - } - } - } -} - -// ============================================================================= -// --- Type 5: Boss --- -// ============================================================================= -// High-HP enemy with phase-based attack patterns. Boss number is set via -// pendingBossNum global before spawn (because spawnEnemy zeroes all ints[]). -// HP is set via pendingBossHp. Both are auto-configured by loadLevel() based -// on the level's bossHp field and level index. -// -// Behavior: -// - Speed 1, bounces off top/bottom. Only attacks when on screen. -// - Attack pattern changes based on remaining HP. HP range is divided into -// N equal phases. Phase 0 = full health, highest phase = near death. -// Later bosses have more phases and higher HP, so they cycle through -// more varied and increasingly aggressive patterns. -// - Each boss (1-5) has a unique updateBoss function with different pattern -// 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 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 -// -// 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 -// ============================================================================= - - - -// 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 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 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 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), + .anim = 6, .speed = FIX32(4), - .anim = enemies[i].clock % 20 == 0 ? 3 : 4 + .angle = enemies[i].fixes[0] - FIX16(15) + FIX16(random() % 30) }; - 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); + 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); - } - } +void loadEnemyThree(u8 i){ + enemies[i].hp = 6; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(1); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 30; + enemies[i].ints[1] = enemies[i].ints[0] + 15; + enemies[i].ints[2] = enemies[i].ints[1] + 10; } - -static void bossPatternSix(u8 i){ - if(enemies[i].clock % 60 == 0){ +void updateEnemyThree(u8 i){ + if(enemies[i].clock % 60 == enemies[i].ints[0]) enemies[i].fixes[0] = enemyHoneAngle(i); + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 >= enemies[i].ints[0] && enemies[i].clock % 60 < enemies[i].ints[1] && enemies[i].clock % 5 == 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); - } - } -} - -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]) + .anim = 3, + .speed = FIX32(5), + .angle = enemies[i].fixes[0] - FIX16(30) }; 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){ + } else if(enemies[i].clock % 60 == enemies[i].ints[2]){ struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, - .anim = 2, - .frame = 1, - .angle = FIX16(random() % 45), - .speed = FIX32(3) + .anim = 1, + .speed = FIX32(4), + .angle = FIX16(random() % 72) }; - for(u8 j = 0; j < 8; j++){ + for(u8 j = 0; j < 5; j++){ spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(72); + } + } +} + +void loadEnemyFour(u8 i){ + enemies[i].hp = 6; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(1); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 30; +} +void updateEnemyFour(u8 i){ + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 == enemies[i].ints[0]) { + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 0, + .angle = FIX16(random() % 45) + }; + void updater(u8 j){ + if(bullets[j].clock == 20){ + // bullets[j].vel.x = 0; + // bullets[j].vel.y = 0; + bullets[j].vel.x = FIX32(player.pos.x < bullets[j].pos.x ? -5 : 5); + bullets[j].vel.y = 0; + } + (void)j; + } + for(u8 j = 0; j < 8; j++){ + // spawner.ints[0] = 10 + F32_toInt(F32_mul(F32_sin(spawner.angle), FIX32(4))); + spawner.vel.x = F32_mul(FIX32(2), F32_cos(spawner.angle)); + spawner.vel.y = F32_mul(FIX32(4), F32_sin(spawner.angle)); + spawner.anim = j % 2 == 0 ? 0 : 1; + spawnBullet(spawner, updater); spawner.angle += FIX16(45); } } } + +// stage 1 boss + +static void bossOnePatternOne(u8 i){ + if(enemies[i].clock % 60 == 30){ + enemies[i].fixes[0] = enemyHoneAngle(i); + enemies[i].fixes[1] = F32_toFix16(enemies[i].pos.x); + enemies[i].fixes[2] = F32_toFix16(enemies[i].pos.y); + } + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 == 0 || enemies[i].clock % 60 == 10){ + struct bulletSpawner spawner = { + .anim = enemies[i].clock % 60 == 0 ? 0 : 1, + .speed = FIX32(4), + .angle = FIX16(random() % 360) + }; + spawner.x = enemies[i].pos.x + F32_mul(F32_cos(spawner.angle), FIX32(32)); + spawner.y = enemies[i].pos.y + F32_mul(F32_sin(spawner.angle), FIX32(32)); + for(u8 j = 0; j < 8; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle = F16_normalizeAngle(spawner.angle + FIX16(45)); + } + } + else if(enemies[i].clock % 60 >= 30 && enemies[i].clock % 60 < 45 && enemies[i].clock % 5 == 0){ + struct bulletSpawner spawner = { + .x = F16_toFix32(enemies[i].fixes[1]), + .y = F16_toFix32(enemies[i].fixes[2]), + .anim = 6, + .speed = FIX32(5), + .angle = enemies[i].fixes[0] + }; + spawnBullet(spawner, EMPTY); + } +} +static void bossOnePatternTwo(u8 i){ + if(enemies[i].clock % 60 == 30){ + enemies[i].fixes[0] = enemyHoneAngle(i); + } + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 9, + .speed = FIX32(6), + .angle = enemyHoneAngle(i) - FIX16(75) + }; + spawner.ints[0] = 0; + void updater(u8 j){ + if(bullets[j].clock > 0 && bullets[j].clock % 2 == 0 && bullets[j].ints[0] == 0){ + bullets[j].speed -= FIX32(0.5); + if(bullets[j].speed <= 0){ + bullets[j].speed = 0; + bullets[j].ints[0] = 1; + bullets[j].ints[1] = 5; + } + updateBulletVel(j); + } else if(bullets[j].ints[0] == 1 && bullets[j].ints[1] > 0){ + bullets[j].ints[1]--; + if(bullets[j].ints[1] == 0){ + bullets[j].angle = bulletHoneAngle(j); + bullets[j].speed = FIX32(6); + updateBulletVel(j); + doBulletRotation(j); + } + } + } + for(u8 j = 0; j < 7; j++){ + spawnBullet(spawner, updater); + spawner.angle += FIX16(25); + } + } else if(enemies[i].clock % 60 >= 30 && enemies[i].clock % 4 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .speed = FIX32(5), + .angle = enemies[i].fixes[0] - FIX16(20) + FIX16(random() % 40) + }; + spawnBullet(spawner, EMPTY); + } +} +static void bossOnePatternThree(u8 i){ + if(enemies[i].clock % 30 == 0){ + enemies[i].fixes[0] = enemyHoneAngle(i); + } + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 30 < 20){ + if(enemies[i].clock % 5 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .speed = FIX32(6), + .angle = enemies[i].fixes[0] - FIX16(5) + FIX16(random() % 10) + }; + spawnBullet(spawner, EMPTY); + } else if(enemies[i].clock % 5 == 2){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = enemies[i].clock % 10 == 2 ? 0 : 1, + .speed = FIX32(4), + .angle = enemies[i].fixes[0] - FIX16(30) + FIX16(random() % 10) + }; + for(u8 j = 0; j < 2; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(50); + } + } + } +} static void updateBossOne(u8 i){ - bossPatternEight(i); - // u8 phase = getBossPhase(i, 2); - // if(phase == 0) bossPatternOne(i); - // else bossPatternTwo(i); + u8 phase = getBossPhase(i, 3); + if(phase == 0) bossOnePatternOne(i); + else if(phase == 1) bossOnePatternTwo(i); + else bossOnePatternThree(i); +} + + +// stage 2 enemies + +void loadEnemyFive(u8 i){ + enemies[i].hp = 2; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(3); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 60; +} +void updateEnemyFive(u8 i){ + if(!enemies[i].onScreen || enemies[i].clock % 60 != enemies[i].ints[0]) return; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .speed = FIX32(5), + .angle = enemyHoneAngle(i) - FIX16(25) + }; + for(u8 j = 0; j < 3; j++){ + spawner.anim = j == 1 ? 1 : 0; + spawnBullet(spawner, EMPTY); + spawner.angle += FIX16(25); + } +} + +void loadEnemySix(u8 i){ + enemies[i].hp = 4; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(1); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 30; + enemies[i].ints[1] = enemies[i].ints[0] + 30; +} +void updateEnemySix(u8 i){ + if(enemies[i].clock % 60 == enemies[i].ints[0]) enemies[i].fixes[0] = FIX16(random() % 360); + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 >= enemies[i].ints[0] && enemies[i].clock % 60 < enemies[i].ints[1] && enemies[i].clock % 2 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = enemies[i].clock % 4 == 0 ? 0 : 1 + }; + spawner.vel.x = F32_mul(FIX32(4), F32_cos(enemies[i].fixes[0])); + spawner.vel.y = F32_mul(FIX32(5), F32_sin(enemies[i].fixes[0])); + spawnBullet(spawner, EMPTY); + enemies[i].fixes[0] = F16_normalizeAngle(enemies[i].fixes[0] + FIX16(enemies[i].clock % 120 < 60 ? 25 : -25)); + } +} + +void loadEnemySeven(u8 i){ + enemies[i].hp = 6; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(1); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 30; + enemies[i].ints[1] = enemies[i].ints[0] + 30; +} +void updateEnemySeven(u8 i){ + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 >= enemies[i].ints[0] && enemies[i].clock % 60 < enemies[i].ints[1] && enemies[i].clock % 2 == 0){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = enemies[i].clock % 4 == 0 ? 0 : 1, + .speed = FIX32(enemies[i].clock % 4 == 0 ? 4 : 3), + .angle = FIX16(random() % 360) + }; + spawnBullet(spawner, EMPTY); + } +} + +void loadEnemyEight(u8 i){ + enemies[i].hp = 6; + enemies[i].angle = FIX16((random() % 4) * 90 + 45); + enemies[i].speed = FIX32(1); + enemies[i].canGrabTreasure = FALSE; + enemies[i].canFlipH = TRUE; + enemies[i].homesOnPlayer = TRUE; + enemies[i].ints[0] = random() % 30; +} +void updateEnemyEight(u8 i){ + if(!enemies[i].onScreen) return; + if(enemies[i].clock % 60 == enemies[i].ints[0]){ + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, + .y = enemies[i].pos.y, + .anim = 1, + .speed = FIX32(3), + .angle = enemyHoneAngle(i) + }; + void updater(u8 j){ + if(bullets[j].clock % 10 == 8){ + struct bulletSpawner spawner = { + .x = bullets[j].pos.x, + .y = bullets[j].pos.y, + .anim = 9, + .speed = FIX32(5) + }; + if(bullets[j].clock % 20 == 8){ + spawner.angle = bullets[j].angle + FIX16(20 + (random() % 20)); + } else { + spawner.angle = bullets[j].angle - FIX16(20 + (random() % 20)); + } + spawnBullet(spawner, EMPTY); + } + } + spawnBullet(spawner, updater); + } +} + + +// stage 3 enemies + +void loadEnemyNine(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyNine(u8 i){ + (void)i; +} + +void loadEnemyTen(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyTen(u8 i){ + (void)i; +} + +void loadEnemyEleven(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyEleven(u8 i){ + (void)i; +} + +void loadEnemyTwelve(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyTwelve(u8 i){ + (void)i; +} + + +// stage 4 enemies + +void loadEnemyThirteen(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyThirteen(u8 i){ + (void)i; +} + +void loadEnemyFourteen(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyFourteen(u8 i){ + (void)i; +} + +void loadEnemyFifteen(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemyFifteen(u8 i){ + (void)i; +} + +void loadEnemySixteen(u8 i){ + enemies[i].hp = 1; + enemies[i].angle = FIX16(random() % 360); + enemies[i].speed = FIX32(0.7); + enemies[i].canGrabTreasure = TRUE; + enemies[i].canFlipH = TRUE; +} +void updateEnemySixteen(u8 i){ + (void)i; } + +// start old stuff -- to be refactored but still working + // 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); + // 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); + // 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); + // 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); + // 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){ @@ -1013,4 +595,311 @@ void updateBoss(u8 i){ case 3: updateBossFour(i); break; case 4: updateBossFive(i); break; } -} \ No newline at end of file +} + + + +// 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 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 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 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 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 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); +// } +// } +// } + +// 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); +// } +// } +// } \ No newline at end of file diff --git a/src/global.h b/src/global.h index 802b974..dbab43a 100644 --- a/src/global.h +++ b/src/global.h @@ -11,7 +11,7 @@ void sfxCollectAllTreasures(); void loadMap(); void loadGame(); -#define SKIP_START 0 +#define SKIP_START 1 #define SKIP_TO_BONUS 0 // 1 = boot straight into bonus stage for testing (0 for release) u32 clock; @@ -21,19 +21,20 @@ u32 clock; #define GAME_H_F FIX32(224) #define SECTION_SIZE FIX32(512) -#define SECTION_COUNT 4 +#define SECTION_COUNT 3 #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 u32 score; u32 highScore; u32 tempHighScore; u32 grazeCount; +bool levelPerfect; u32 nextExtendScore; #define EXTEND_SCORE 25000 #define SCORE_LENGTH 8 @@ -118,6 +119,11 @@ struct playerStruct { 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) + u8 bombCount; // 0-2 + u8 activePowerup; // 0=none, 1=spread, 2=rapid + u16 powerupClock; // countdown + bool hasShield; + u16 shieldClock; // countdown (max 1800) fix32 camera; Sprite* image; }; @@ -138,7 +144,7 @@ struct bulletSpawner { }; struct bullet { fix32 speed; - bool active, player, vFlip, hFlip, explosion, grazed; + bool active, player, vFlip, hFlip, grazed; Vect2D_f32 pos, vel; Sprite* image; s16 clock, angle, anim, frame; @@ -152,12 +158,66 @@ struct bullet bullets[BULLET_COUNT]; // enemies #define ENEMY_COUNT 24 -#define ENEMY_TYPE_TEST 0 -#define ENEMY_TYPE_DRONE 1 -#define ENEMY_TYPE_GUNNER 2 -#define ENEMY_TYPE_HUNTER 3 -#define ENEMY_TYPE_BUILDER 4 -#define ENEMY_TYPE_BOSS 5 +#define ENEMY_TYPE_ONE 0 +#define ENEMY_TYPE_TWO 1 +#define ENEMY_TYPE_THREE 2 +#define ENEMY_TYPE_FOUR 3 +#define ENEMY_TYPE_FIVE 4 +#define ENEMY_TYPE_SIX 5 +#define ENEMY_TYPE_SEVEN 6 +#define ENEMY_TYPE_EIGHT 7 +#define ENEMY_TYPE_NINE 8 +#define ENEMY_TYPE_TEN 9 +#define ENEMY_TYPE_ELEVEN 10 +#define ENEMY_TYPE_TWELVE 11 +#define ENEMY_TYPE_THIRTEEN 12 +#define ENEMY_TYPE_FOURTEEN 13 +#define ENEMY_TYPE_FIFTEEN 14 +#define ENEMY_TYPE_SIXTEEN 15 +#define ENEMY_TYPE_BOSS 16 + +#define ENEMY_TYPE_COUNT 16 // number of shoppable types (excludes boss) + +typedef struct { + u8 type; // ENEMY_TYPE_* constant + u8 cost; // TP cost + u8 weight; // shopping probability weight + u8 maxCount; // max per level + u8 minCount; // guaranteed minimum per level + u8 unlockLevel; // first level index where this type can appear +} EnemyTypeDef; + +// cost: how many threat points this enemy costs to place (higher = fewer spawned) +// weight: how likely this type is to be picked each shopping roll (higher = more common) +// max: hard cap per level (won't exceed this even with remaining budget) +// min: guaranteed spawns before shopping starts (cost deducted from budget) +// unlock: first level index where this type enters the pool (0 = available immediately) +static const EnemyTypeDef enemyTypeDefs[ENEMY_TYPE_COUNT] = { + // cost weight max min unlock + { ENEMY_TYPE_ONE, 1, 10, 2, 0, 5 }, + { ENEMY_TYPE_TWO, 1, 10, 2, 0, 5 }, + { ENEMY_TYPE_THREE, 1, 10, 2, 0, 5 }, + { ENEMY_TYPE_FOUR, 1, 10, 2, 0, 5 }, + { ENEMY_TYPE_FIVE, 1, 10, 2, 0, 5 }, + { ENEMY_TYPE_SIX, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_SEVEN, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_EIGHT, 1, 10, 5, 0, 0 }, + { ENEMY_TYPE_NINE, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_TEN, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_ELEVEN, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_TWELVE, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_THIRTEEN, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_FOURTEEN, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_FIFTEEN, 1, 10, 5, 0, 5 }, + { ENEMY_TYPE_SIXTEEN, 1, 10, 5, 0, 5 }, +}; + +// Threat point budget formula: base + (lvl * linear) + (lvl * lvl / quadratic) +// Then randomized to 90-110%. Boss levels get bossPercent% of that. +#define TP_BASE 8 +#define TP_LINEAR 4 +#define TP_QUADRATIC 5 +#define TP_BOSS_PCT 40 struct enemy { bool active, onScreen; @@ -168,6 +228,13 @@ struct enemy { fix32 speed; Vect2D_f32 vel, pos; Sprite* image; + bool canGrabTreasure; + bool homesOnPlayer; + bool canShoot; + bool canFlipH; + bool useBigSprite; + s16 carriedTreasure; + s16 targetTreasure; s16 ints[PROP_COUNT]; fix16 fixes[PROP_COUNT]; }; @@ -196,57 +263,36 @@ u16 levelEnemiesKilled; u16 statEnemiesKilled; s16 statTreasures; -void killTreasure(u8 i){ - if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){ - enemies[treasures[i].carriedBy].ints[3] = -1; - treasureBeingCarried = FALSE; - } - treasures[i].active = FALSE; - SPR_releaseSprite(treasures[i].image); +void removeShieldVisual(){ + SPR_setDefinition(player.image, &momoyoSprite); } -void killBullet(u8 i, bool explode){ - if(explode){ - s16 a = bullets[i].anim; - s16 explosionAnim; - if(bullets[i].player){ - explosionAnim = 16; - } else if(a < FIRST_ROTATING_BULLET){ - explosionAnim = 13 + bullets[i].frame; - } else { - 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; - SPR_setFrame(bullets[i].image, 0); - SPR_setHFlip(bullets[i].image, random() & 1); - // SPR_setVFlip(bullets[i].image, random() & 1); - } else { - bullets[i].active = FALSE; - SPR_releaseSprite(bullets[i].image); - } -} +// pickups +#define PICKUP_COUNT 1 +#define PICKUP_TYPE_BOMB 0 +#define PICKUP_TYPE_SPREAD 1 +#define PICKUP_TYPE_RAPID 2 +#define PICKUP_TYPE_SHIELD 3 +#define PICKUP_SPAWN_INTERVAL 450 // ~15 sec (called on even frames only) +#define PICKUP_LIFETIME 300 // ~10 sec (called on even frames only) +#define PICKUP_BLINK_START 30 // blink final ~1 sec (even frames only) +#define BOMB_MAX 2 +#define BOMB_DAMAGE 8 +#define BOMB_BOSS_DAMAGE 4 +#define BOMB_IFRAMES 60 +#define BOMB_BULLET_SCORE 32 +#define POWERUP_DURATION 600 +#define SHIELD_TIMEOUT 600 +#define PICKUP_OFF 8 -void killEnemy(u8 i){ - if(isAttract) return; - enemies[i].hp--; - if(enemies[i].hp > 0) return; - if(enemies[i].ints[3] >= 0){ - s16 h = enemies[i].ints[3]; - if(treasures[h].active){ - treasures[h].state = TREASURE_FALLING; - treasures[h].carriedBy = -1; - treasures[h].vel.x = 0; - treasures[h].vel.y = FIX32(3); - } - treasureBeingCarried = FALSE; - } - enemies[i].active = FALSE; - SPR_releaseSprite(enemies[i].image); - levelEnemiesKilled++; +struct pickup { bool active; u8 type; u16 lifeClock; Vect2D_f32 pos; Sprite* image; }; +struct pickup pickups[PICKUP_COUNT]; +u16 pickupSpawnClock; + +void killPickup(u8 i){ + if(!pickups[i].active) return; + pickups[i].active = FALSE; + SPR_releaseSprite(pickups[i].image); } static fix32 getWrappedDelta(fix32 a, fix32 b) { @@ -269,6 +315,122 @@ static s16 getScreenX(fix32 worldX, fix32 camera) { return F32_toInt(screenX); } +void killTreasure(u8 i){ + if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){ + enemies[treasures[i].carriedBy].carriedTreasure = -1; + treasureBeingCarried = FALSE; + } + treasures[i].active = FALSE; + SPR_releaseSprite(treasures[i].image); +} + +// explosion pool (shared by all explosions: bullet, enemy, player death) +#define EXPLOSION_COUNT 12 + +struct explosion { + bool active, big; + u8 frame, clock; + fix32 x, y; + Sprite* image; +}; +struct explosion explosions[EXPLOSION_COUNT]; + +void spawnExplosion(fix32 x, fix32 y, u8 anim, bool big){ + fix32 dx = getWrappedDelta(x, player.pos.x); + if(dx < -CULL_LIMIT || dx > CULL_LIMIT) return; + s16 slot = -1; + for(s16 j = 0; j < EXPLOSION_COUNT; j++) if(!explosions[j].active){ slot = j; break; } + if(slot < 0) return; + explosions[slot].active = TRUE; + explosions[slot].big = big; + explosions[slot].x = x; + explosions[slot].y = y; + explosions[slot].frame = 0; + explosions[slot].clock = 0; + SpriteDefinition const* def = big ? &explosionBigSprite : &explosionsSprite; + explosions[slot].image = SPR_addSprite(def, -64, -64, TILE_ATTR(PAL0, 0, 0, 0)); + if(!explosions[slot].image){ + explosions[slot].active = FALSE; + return; + } + SPR_setDepth(explosions[slot].image, big ? 0 : 5); + SPR_setAnim(explosions[slot].image, anim); + SPR_setFrame(explosions[slot].image, 0); + SPR_setHFlip(explosions[slot].image, random() & 1); + s16 sx = getScreenX(x, player.camera); + s16 sy = F32_toInt(y); + u8 off = big ? 32 : 16; + SPR_setPosition(explosions[slot].image, sx - off, sy - off); +} + +void updateExplosions(){ + for(s16 i = 0; i < EXPLOSION_COUNT; i++){ + if(!explosions[i].active) continue; + explosions[i].clock++; + if(explosions[i].clock % 4 == 0){ + explosions[i].frame++; + if(explosions[i].frame >= 5){ + SPR_releaseSprite(explosions[i].image); + explosions[i].active = FALSE; + continue; + } + SPR_setFrame(explosions[i].image, explosions[i].frame); + } + s16 sx = getScreenX(explosions[i].x, player.camera); + s16 sy = F32_toInt(explosions[i].y); + u8 off = explosions[i].big ? 32 : 16; + SPR_setPosition(explosions[i].image, sx - off, sy - off); + } +} + +void clearExplosions(){ + for(s16 i = 0; i < EXPLOSION_COUNT; i++){ + if(explosions[i].active){ + SPR_releaseSprite(explosions[i].image); + explosions[i].active = FALSE; + } + } +} + +static u8 getBulletExplosionAnim(u8 i){ + if(bullets[i].player) return 3; // yellow + if(bullets[i].anim < FIRST_ROTATING_BULLET) return bullets[i].frame; // 0=blue, 1=red, 2=green + return (bullets[i].anim - FIRST_ROTATING_BULLET) % 3; // rotating: 0=blue, 1=red, 2=green +} + +void killBullet(u8 i, bool explode){ + if(explode){ + spawnExplosion(bullets[i].pos.x, bullets[i].pos.y, getBulletExplosionAnim(i), FALSE); + } + bullets[i].active = FALSE; + SPR_releaseSprite(bullets[i].image); +} + +void killEnemy(u8 i){ + if(isAttract) return; + enemies[i].hp--; + if(enemies[i].hp > 0){ + // enemy hit but not dead — small yellow explosion + spawnExplosion(enemies[i].pos.x, enemies[i].pos.y, 3, FALSE); + return; + } + // enemy killed — big explosion + spawnExplosion(enemies[i].pos.x, enemies[i].pos.y, 3, TRUE); + if(enemies[i].carriedTreasure >= 0){ + s16 h = enemies[i].carriedTreasure; + if(treasures[h].active){ + treasures[h].state = TREASURE_FALLING; + treasures[h].carriedBy = -1; + treasures[h].vel.x = 0; + treasures[h].vel.y = FIX32(3); + } + treasureBeingCarried = FALSE; + } + enemies[i].active = FALSE; + SPR_releaseSprite(enemies[i].image); + levelEnemiesKilled++; +} + // homing -- degree-based using SGDK F16_atan2 (returns fix16 degrees) static fix16 getAngle(fix32 dx, fix32 dy){ diff --git a/src/main.c b/src/main.c index a6f1959..cc77fd1 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,7 @@ #include "bullets.h" #include "enemies.h" #include "treasure.h" +#include "pickup.h" #include "player.h" #include "stage.h" #include "chrome.h" @@ -44,6 +45,10 @@ void clearLevel(){ if(enemies[i].active){ enemies[i].hp = 0; killEnemy(i); } for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) killTreasure(i); + clearExplosions(); + clearPickups(); + clearPopups(); + removeShieldVisual(); treasureBeingCarried = FALSE; collectedCount = 0; allTreasureCollected = FALSE; @@ -64,12 +69,20 @@ void loadGame(){ loadChrome(); loadLevel(isAttract ? ATTRACT_LEVEL : START_LEVEL); #if MUSIC_VOLUME > 0 - if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic); + if(!isAttract) XGM2_play(stageMusic); #endif player.recoveringClock = 240; player.recoverFlash = FALSE; killBullets = TRUE; attractEnding = FALSE; + player.bombCount = 0; + player.activePowerup = 0; + player.powerupClock = 0; + player.hasShield = FALSE; + player.shieldClock = 0; + pickupSpawnClock = PICKUP_SPAWN_INTERVAL; + bombUsing = FALSE; + bombFlashClock = 0; started = TRUE; #if SKIP_TO_BONUS clearLevel(); @@ -110,6 +123,7 @@ static void updateGame(){ // Bonus stage branch (after boss fights) if(bonusStage){ updateBonus(); + updateExplosions(); if(!bonusActive){ bonusStage = FALSE; clearBonus(); @@ -117,6 +131,12 @@ static void updateGame(){ player.pos.y = FIX32(112); player.camera = player.pos.x - FIX32(160); playerVelX = 0; + // Re-allocate player sprite (released by loadBonus) + player.image = SPR_addSprite(&momoyoSprite, + -48, -48, + TILE_ATTR(PAL0, 0, 0, 0)); + SPR_setDepth(player.image, 0); + SPR_setVisibility(player.image, HIDDEN); loadBackground(); loadChrome(); loadLevel(level + 1); @@ -125,9 +145,14 @@ static void updateGame(){ player.recoveringClock = 240; player.recoverFlash = FALSE; killBullets = TRUE; + player.activePowerup = 0; + player.powerupClock = 0; + player.hasShield = FALSE; + player.shieldClock = 0; + removeShieldVisual(); XGM2_stop(); #if MUSIC_VOLUME > 0 - if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic); + if(!isAttract) XGM2_play(stageMusic); #endif } return; @@ -140,6 +165,7 @@ static void updateGame(){ if(isBossLevel(level) && !isAttract){ loadBonus(level % 3); bonusStage = TRUE; + VDP_drawText("Entering Bonus Level", 10, 14); } else { loadStarfield(level % 3); } @@ -164,12 +190,17 @@ static void updateGame(){ player.recoveringClock = 240; player.recoverFlash = FALSE; killBullets = TRUE; + player.activePowerup = 0; + player.powerupClock = 0; + player.hasShield = FALSE; + player.shieldClock = 0; XGM2_stop(); #if MUSIC_VOLUME > 0 - if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic); + if(!isAttract) XGM2_play(stageMusic); #endif } if(levelClearing && !bonusStage) updateStarfield(); + updateExplosions(); return; } if(levelWaitClock > 0){ @@ -205,10 +236,12 @@ static void updateGame(){ } } updateTreasures(); + updatePickups(); } else { updateBullets(); } } + updateExplosions(); } int main(bool hardReset){ diff --git a/src/pickup.h b/src/pickup.h new file mode 100644 index 0000000..839d055 --- /dev/null +++ b/src/pickup.h @@ -0,0 +1,154 @@ +static u8 pickPickupType(){ + u8 weights[4]; + u8 total; + if(player.bombCount < BOMB_MAX){ + weights[0] = 2; weights[1] = 4; weights[2] = 4; weights[3] = 3; + total = 13; + } else { + weights[0] = 0; weights[1] = 5; weights[2] = 5; weights[3] = 3; + total = 13; + } + u8 roll = random() % total; + u8 accum = 0; + for(u8 t = 0; t < 4; t++){ + accum += weights[t]; + if(roll < accum) return t; + } + return PICKUP_TYPE_SPREAD; +} + +static void spawnPickup(){ + s16 slot = -1; + for(s16 i = 0; i < PICKUP_COUNT; i++) if(!pickups[i].active){ slot = i; break; } + if(slot < 0) return; + + // random on-screen position with 48px margin + s16 screenPosX = 48 + (random() % (320 - 96)); + s16 screenPosY = 48 + (random() % (224 - 96)); + + // convert screen coords to world coords + fix32 worldX = player.camera + FIX32(screenPosX); + if(worldX >= GAME_WRAP) worldX -= GAME_WRAP; + if(worldX < 0) worldX += GAME_WRAP; + + pickups[slot].active = TRUE; + pickups[slot].type = pickPickupType(); + pickups[slot].lifeClock = PICKUP_LIFETIME; + pickups[slot].pos.x = worldX; + pickups[slot].pos.y = FIX32(screenPosY); + + pickups[slot].image = SPR_addSprite(&pickupSprite, + screenPosX - PICKUP_OFF, screenPosY - PICKUP_OFF, + TILE_ATTR(PAL0, 0, 0, 0)); + if(!pickups[slot].image){ + pickups[slot].active = FALSE; + return; + } + SPR_setDepth(pickups[slot].image, 1); + SPR_setFrame(pickups[slot].image, pickups[slot].type); +} + +static void collectPickup(u8 i){ + if(isAttract){ + killPickup(i); + return; + } + switch(pickups[i].type){ + case PICKUP_TYPE_BOMB: + if(player.bombCount >= BOMB_MAX){ + score += 2048; + spawnPopup(pickups[i].pos.x, pickups[i].pos.y, 2048); + } else { + player.bombCount++; + } + break; + case PICKUP_TYPE_SPREAD: + player.activePowerup = 1; + player.powerupClock = POWERUP_DURATION; + break; + case PICKUP_TYPE_RAPID: + player.activePowerup = 2; + player.powerupClock = POWERUP_DURATION; + break; + case PICKUP_TYPE_SHIELD: + player.hasShield = TRUE; + player.shieldClock = SHIELD_TIMEOUT; + SPR_setDefinition(player.image, &shieldSprite); + break; + } + sfxPickup(); + killPickup(i); +} + +static void updatePickup(u8 i){ + pickups[i].lifeClock--; + + // blink in final frames + if(pickups[i].lifeClock <= PICKUP_BLINK_START){ + SPR_setVisibility(pickups[i].image, + (pickups[i].lifeClock / 4) % 2 == 0 ? HIDDEN : VISIBLE); + } + + if(pickups[i].lifeClock == 0){ + killPickup(i); + return; + } + + // scroll with world + pickups[i].pos.x -= (player.vel.x >> 3); + pickups[i].pos.y -= (playerScrollVelY >> 3); + + // X wrap + if(pickups[i].pos.x >= GAME_WRAP) pickups[i].pos.x -= GAME_WRAP; + if(pickups[i].pos.x < 0) pickups[i].pos.x += GAME_WRAP; + + // collection check (32px box, same as treasure) + if(!isAttract && player.respawnClock == 0){ + fix32 dx = getWrappedDelta(pickups[i].pos.x, player.pos.x); + fix32 dy = pickups[i].pos.y - player.pos.y; + if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){ + collectPickup(i); + return; + } + } + + // update sprite position + visibility + s16 sx = getScreenX(pickups[i].pos.x, player.camera); + s16 sy = F32_toInt(pickups[i].pos.y); + fix32 ddx = getWrappedDelta(pickups[i].pos.x, player.pos.x); + bool onScreen = (ddx >= -CULL_LIMIT && ddx <= CULL_LIMIT); + + if(onScreen){ + SPR_setPosition(pickups[i].image, sx - PICKUP_OFF, sy - PICKUP_OFF); + // only override visibility if not already blinking + if(pickups[i].lifeClock > PICKUP_BLINK_START) + SPR_setVisibility(pickups[i].image, VISIBLE); + } else { + SPR_setVisibility(pickups[i].image, HIDDEN); + } +} + +void updatePickups(){ + // check if any pickup is active + bool anyActive = FALSE; + for(s16 i = 0; i < PICKUP_COUNT; i++){ + if(pickups[i].active){ + anyActive = TRUE; + updatePickup(i); + } + } + + // spawn timer + if(!anyActive && !levelClearing && levelWaitClock == 0 && !gameOver){ + if(pickupSpawnClock > 0) pickupSpawnClock--; + if(pickupSpawnClock == 0){ + spawnPickup(); + pickupSpawnClock = PICKUP_SPAWN_INTERVAL; + } + } +} + +void clearPickups(){ + for(s16 i = 0; i < PICKUP_COUNT; i++) + if(pickups[i].active) killPickup(i); +} diff --git a/src/player.h b/src/player.h index 0dd7c58..ad7625b 100644 --- a/src/player.h +++ b/src/player.h @@ -18,6 +18,12 @@ fix32 screenX; fix32 playerSpeed; fix32 playerVelX; +bool bombUsing; +u8 bombFlashClock; +u16 storedPal0[16]; +u16 storedPal1[16]; +u16 storedPal2[16]; + static void movePlayer(){ player.vel.y = 0; @@ -93,8 +99,8 @@ static void cameraPlayer(){ } static void shootPlayer(){ + s16 interval = (player.activePowerup == 2) ? (SHOT_INTERVAL / 2) : SHOT_INTERVAL; 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, @@ -109,11 +115,67 @@ static void shootPlayer(){ if(bullets[i].clock == 4) killBullet(i, TRUE); } spawnBullet(spawner, updater); + + // spread shot: 2 additional bullets at ±15° + if(player.activePowerup == 1){ + for(s16 s = -1; s <= 1; s += 2){ + s16 spreadAngle = F16_normalizeAngle(player.shotAngle + FIX16(15) * s); + struct bulletSpawner sp = { + .x = player.pos.x, + .y = player.pos.y, + .anim = 12, + .speed = PLAYER_SHOT_SPEED, + .angle = spreadAngle, + .player = TRUE + }; + sp.ints[5] = F32_toInt(F32_mul(F32_cos(spreadAngle), PLAYER_SHOT_SPEED)); + void spreadUpdater(s16 j){ + bullets[j].vel.x = FIX32(bullets[j].ints[5]) + (player.vel.x << 2); + if(bullets[j].clock == 4) killBullet(j, TRUE); + } + spawnBullet(sp, spreadUpdater); + } + } + sfxPlayerShot(); - shotClock = SHOT_INTERVAL; + shotClock = interval; } else if(shotClock > 0) shotClock--; } +static void activateBomb(){ + // explode all on-screen enemy bullets + for(s16 i = 0; i < BULLET_COUNT; i++){ + if(!bullets[i].active || bullets[i].player) continue; + fix32 dx = getWrappedDelta(bullets[i].pos.x, player.pos.x); + if(dx >= -CULL_LIMIT && dx <= CULL_LIMIT){ + score += BOMB_BULLET_SCORE; + killBullet(i, TRUE); + } + } + // damage on-screen enemies + for(s16 i = 0; i < ENEMY_COUNT; i++){ + if(!enemies[i].active || !enemies[i].onScreen) continue; + s16 dmg = (enemies[i].type == ENEMY_TYPE_BOSS) ? BOMB_BOSS_DAMAGE : BOMB_DAMAGE; + for(s16 d = 0; d < dmg; d++){ + if(!enemies[i].active) break; + killEnemy(i); + } + } + // i-frames (no flash) + player.recoveringClock = BOMB_IFRAMES; + player.recoverFlash = FALSE; + // screen flash + bombFlashClock = 4; + // save current palettes for restore + memcpy(storedPal0, font.palette->data, 16 * sizeof(u16)); + memcpy(storedPal1, shadow.palette->data, 16 * sizeof(u16)); + memcpy(storedPal2, bgPal, 16 * sizeof(u16)); + u16 whitePal[48]; + for(s16 i = 0; i < 48; i++) whitePal[i] = 0x0EEE; + PAL_setColors(0, whitePal, 48, CPU); + sfxExplosion(); +} + static s16 attractXClock = 0; static s16 attractXState = 0; // 0=moving, 1=paused static s16 attractXDir = 1; // 1=right, -1=left @@ -186,7 +248,36 @@ void updatePlayer(){ waitForRelease = FALSE; return; } + // bomb flash restore + if(bombFlashClock > 0){ + bombFlashClock--; + if(bombFlashClock == 0){ + PAL_setColors(0, storedPal0, 16, CPU); + PAL_setColors(16, storedPal1, 16, CPU); + PAL_setColors(32, storedPal2, 16, CPU); + } + } + // powerup timer tick + if(player.powerupClock > 0){ + player.powerupClock--; + if(player.powerupClock == 0) player.activePowerup = 0; + } + if(player.shieldClock > 0){ + player.shieldClock--; + if(player.shieldClock == 0){ + player.hasShield = FALSE; + removeShieldVisual(); + } + } if(!gameOver){ + // bomb activation + if(ctrl.b && !bombUsing && player.bombCount > 0 && player.respawnClock == 0){ + bombUsing = TRUE; + player.bombCount--; + activateBomb(); + } + if(!ctrl.b) bombUsing = FALSE; + if(player.respawnClock > 0){ // kill momentum playerVelX = 0; @@ -203,6 +294,7 @@ void updatePlayer(){ s16 sx = getScreenX(player.pos.x, player.camera); s16 sy = F32_toInt(player.pos.y); SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); + // shield visual is part of player.image now, hidden along with it player.respawnClock--; if(player.respawnClock == 0){ SPR_setVisibility(player.image, VISIBLE); @@ -227,6 +319,11 @@ void updatePlayer(){ s16 sx = getScreenX(player.pos.x, player.camera); s16 sy = F32_toInt(player.pos.y); SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); + // shield blink: alternate between shield and momoyo sprites in final 150 frames + if(player.hasShield && player.shieldClock <= 150){ + SpriteDefinition const* def = (player.shieldClock / 6) % 2 == 0 ? &momoyoSprite : &shieldSprite; + SPR_setDefinition(player.image, def); + } if(player.pendingShow){ SPR_setVisibility(player.image, VISIBLE); player.pendingShow = FALSE; diff --git a/src/stage.h b/src/stage.h index 1108887..8124e68 100644 --- a/src/stage.h +++ b/src/stage.h @@ -13,42 +13,9 @@ // ============================================================================= #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 -}; - -// 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 }; // Boss HP per boss number (0-4) -static const s16 bossHpTable[5] = { 24, 50, 75, 100, 125 }; - -// 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; -} +static const s16 bossHpTable[5] = { 20, 40, 60, 80, 120 }; static u8 getTreasureCount(u8 lvl){ if(lvl == 0) return 4; @@ -56,22 +23,6 @@ static u8 getTreasureCount(u8 lvl){ return 8; } -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 - - 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 % SECTION_COUNT; @@ -79,34 +30,28 @@ static void distributeEnemies(u8 type, u8 count){ } } -// Generate enemy counts into the provided array (indexed by pool index) +// Generate enemy counts into the provided array (indexed by enemyTypeDefs 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; - } + for(u8 i = 0; i < ENEMY_TYPE_COUNT; i++) counts[i] = 0; // Compute budget - u16 budget = 8 + (lvl * 4) + (lvl * lvl / 5); + u16 budget = TP_BASE + (lvl * TP_LINEAR) + (lvl * lvl / TP_QUADRATIC); budget = (budget * (90 + (random() % 21))) / 100; - // Boss levels get 40% escort budget + // Boss levels get reduced escort budget if(isBossLevel(lvl)){ - budget = (budget * 40) / 100; + budget = (budget * TP_BOSS_PCT) / 100; if(budget < 4) budget = 4; } - u8 unlocked = getUnlockedTypes(lvl); - u8 maxTotal = isBossLevel(lvl) ? ENEMY_COUNT - 1 : ENEMY_COUNT; // reserve 1 slot for boss + bool boss = isBossLevel(lvl); + u8 maxTotal = boss ? 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]; + for(u8 i = 0; i < ENEMY_TYPE_COUNT; i++){ + if(lvl >= enemyTypeDefs[i].unlockLevel && enemyTypeDefs[i].minCount > 0){ + counts[i] = enemyTypeDefs[i].minCount; + u16 cost = enemyTypeDefs[i].minCount * enemyTypeDefs[i].cost; if(cost <= budget) budget -= cost; else budget = 0; } @@ -115,22 +60,18 @@ static void generateLevel(u8 lvl, u8* counts){ // Shopping loop u16 safety = 0; u8 totalEnemies = 0; - for(u8 i = 0; i < TP_POOL_SIZE; i++) totalEnemies += counts[i]; + for(u8 i = 0; i < ENEMY_TYPE_COUNT; 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]; + for(u8 i = 0; i < ENEMY_TYPE_COUNT; i++){ + if(lvl < enemyTypeDefs[i].unlockLevel) continue; + if(counts[i] >= enemyTypeDefs[i].maxCount) continue; + if(enemyTypeDefs[i].cost > budget) continue; + totalWeight += enemyTypeDefs[i].weight; } if(totalWeight == 0) break; @@ -138,12 +79,12 @@ static void generateLevel(u8 lvl, u8* counts){ 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]; + for(u8 i = 0; i < ENEMY_TYPE_COUNT; i++){ + if(lvl < enemyTypeDefs[i].unlockLevel) continue; + if(boss && enemyTypeDefs[i].type != ENEMY_TYPE_TWO && enemyTypeDefs[i].type != ENEMY_TYPE_FIVE) continue; + if(counts[i] >= enemyTypeDefs[i].maxCount) continue; + if(enemyTypeDefs[i].cost > budget) continue; + accum += enemyTypeDefs[i].weight; if(roll < accum){ picked = i; break; @@ -152,7 +93,7 @@ static void generateLevel(u8 lvl, u8* counts){ if(picked == 0xFF) break; counts[picked]++; - budget -= typeCost[picked]; + budget -= enemyTypeDefs[picked].cost; totalEnemies++; } } @@ -161,20 +102,18 @@ void loadLevel(u8 lvl){ if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1; level = lvl; grazeCount = 0; + levelPerfect = TRUE; // Generate enemy composition - u8 counts[TP_POOL_SIZE]; + u8 counts[ENEMY_TYPE_COUNT]; generateLevel(lvl, counts); // Spawn enemies by type - for(u8 i = 0; i < TP_POOL_SIZE; i++){ + for(u8 i = 0; i < ENEMY_TYPE_COUNT; i++){ if(counts[i] > 0) - distributeEnemies(poolTypeMap[i], counts[i]); + distributeEnemies(enemyTypeDefs[i].type, counts[i]); } - // Assign gunner patterns - assignGunnerPatterns(lvl); - // Boss spawn if(isBossLevel(lvl)){ pendingBossNum = lvl / 3; @@ -183,13 +122,15 @@ void loadLevel(u8 lvl){ spawnEnemy(ENEMY_TYPE_BOSS, 1); } - // 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); - treasureToSpawn--; + // Spawn treasures (not on boss levels) + if(!isBossLevel(lvl)){ + 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); + treasureToSpawn--; + } } } diff --git a/src/treasure.h b/src/treasure.h index 02b9cf7..0d28d33 100644 --- a/src/treasure.h +++ b/src/treasure.h @@ -115,7 +115,9 @@ 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) ? 4096 : 1024; + u32 pts = (treasures[i].state == TREASURE_FALLING) ? 4096 : 1024; + score += pts; + spawnPopup(treasures[i].pos.x, treasures[i].pos.y, pts); // check if this is the last treasure (all others inactive or collected) bool willBeLast = TRUE; for(s16 j = 0; j < TREASURE_COUNT; j++){ diff --git a/tools/downscale_sprite.py b/tools/downscale_sprite.py new file mode 100644 index 0000000..9eaa8e6 --- /dev/null +++ b/tools/downscale_sprite.py @@ -0,0 +1,540 @@ +#!/usr/bin/env python3 +"""Content-adaptive sprite downscaler (Kopf-Shamir-Peers 2013). + +Downscales hand-drawn sprites into Genesis-compatible indexed-color pixel art +using bilateral EM kernels that respect edges and color boundaries. + +Dependencies: pip install numpy scikit-image Pillow +""" + +import argparse +import sys +import numpy as np +from pathlib import Path + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +GENESIS_LEVELS = np.array([0, 36, 72, 109, 145, 182, 218, 255], dtype=np.uint8) + + +# --------------------------------------------------------------------------- +# Color utilities +# --------------------------------------------------------------------------- + +def rgb_to_lab(rgb: np.ndarray) -> np.ndarray: + """Convert float RGB [0,1] image to CIELAB. Uses skimage.""" + from skimage.color import rgb2lab + return rgb2lab(rgb) + + +def lab_to_rgb(lab: np.ndarray) -> np.ndarray: + """Convert CIELAB image to float RGB [0,1]. Uses skimage.""" + from skimage.color import lab2rgb + return lab2rgb(lab) + + +def normalize_lab(lab: np.ndarray) -> np.ndarray: + """Normalize CIELAB to roughly [0,1] per channel (matching paper).""" + out = np.empty_like(lab) + out[..., 0] = lab[..., 0] / 100.0 + out[..., 1] = (lab[..., 1] + 87.0) / 186.0 + out[..., 2] = (lab[..., 2] + 108.0) / 203.0 + return out + + +def denormalize_lab(nlab: np.ndarray) -> np.ndarray: + """Undo normalize_lab.""" + out = np.empty_like(nlab) + out[..., 0] = nlab[..., 0] * 100.0 + out[..., 1] = nlab[..., 1] * 186.0 - 87.0 + out[..., 2] = nlab[..., 2] * 203.0 - 108.0 + return out + + +# --------------------------------------------------------------------------- +# Image I/O +# --------------------------------------------------------------------------- + +def load_input(path: str, bg_color=None): + """Load PNG, return (normalized_lab, alpha_mask).""" + from PIL import Image + + img = Image.open(path) + if img.mode == "RGBA": + arr = np.array(img, dtype=np.float64) + alpha_mask = arr[:, :, 3] > 127 + rgb01 = arr[:, :, :3] / 255.0 + elif img.mode == "RGB": + arr = np.array(img, dtype=np.float64) + rgb01 = arr / 255.0 + if bg_color is not None: + bg = np.array(bg_color, dtype=np.float64) / 255.0 + alpha_mask = ~np.all(np.abs(rgb01 - bg) < 1e-3, axis=-1) + else: + alpha_mask = np.ones(rgb01.shape[:2], dtype=bool) + else: + img = img.convert("RGBA") + arr = np.array(img, dtype=np.float64) + alpha_mask = arr[:, :, 3] > 127 + rgb01 = arr[:, :, :3] / 255.0 + + lab = rgb_to_lab(rgb01) + nlab = normalize_lab(lab) + return nlab, alpha_mask + + +def load_palette(path: str): + """Load a 16-color palette from an indexed PNG. + + Returns (16, 3) uint8 RGB array. + """ + from PIL import Image + + img = Image.open(path) + if img.mode != "P": + print(f"Error: palette image must be indexed-color (mode P), got {img.mode}", + file=sys.stderr) + sys.exit(1) + + raw_pal = img.getpalette() + if raw_pal is None: + print("Error: palette image has no palette data", file=sys.stderr) + sys.exit(1) + + pal = np.array(raw_pal[:48], dtype=np.uint8).reshape(16, 3) + return pal + + +def save_indexed_png(path: str, pixels: np.ndarray, palette: np.ndarray): + """Write indexed-color PNG with the palette exactly as provided.""" + from PIL import Image + + img = Image.fromarray(pixels, mode="P") + flat_pal = palette.flatten().tolist() + flat_pal.extend([0] * (768 - len(flat_pal))) + img.putpalette(flat_pal) + img.save(path) + + +# --------------------------------------------------------------------------- +# EM core +# --------------------------------------------------------------------------- + +def _invert_2x2(m): + """Invert a single 2x2 matrix.""" + a, b, c, d = m[0, 0], m[0, 1], m[1, 0], m[1, 1] + det = a * d - b * c + if abs(det) < 1e-15: + det = 1e-15 + return np.array([[d, -b], [-c, a]], dtype=np.float64) / det + + +def run_downscale(nlab_image, alpha_mask, out_h, out_w, n_iters=30, + verbose=False): + """Run full EM downscale with proper per-pixel normalization. + + nlab_image: (H, W, 3) normalized CIELAB [0,1] + alpha_mask: (H, W) bool + + Returns (output_nlab, output_alpha). + """ + in_h, in_w = nlab_image.shape[:2] + K = out_h * out_w + N = in_h * in_w + rx = in_w / out_w + ry = in_h / out_h + + # Flatten input for indexing + nlab_flat = nlab_image.reshape(N, 3) + alpha_flat = alpha_mask.ravel() + + # Precompute all input pixel coordinates (row, col) in input space + grid_y, grid_x = np.mgrid[:in_h, :in_w] + coords_flat = np.stack([grid_x.ravel(), grid_y.ravel()], axis=-1).astype(np.float64) + + # --- Initialization --- + + # Output pixel centers in input coordinates + oy, ox = np.meshgrid( + (np.arange(out_h) + 0.5) * ry, + (np.arange(out_w) + 0.5) * rx, + indexing="ij", + ) + centers = np.stack([ox.ravel(), oy.ravel()], axis=-1) # (K, 2) + + mu = centers.copy() + # Covariance: diag(rx/3, ry/3) — NOT squared, matching paper + Sigma = np.zeros((K, 2, 2), dtype=np.float64) + Sigma[:, 0, 0] = rx / 3.0 + Sigma[:, 1, 1] = ry / 3.0 + + # Color mean: local average from input + nu = np.full((K, 3), 0.5, dtype=np.float64) + for k in range(K): + cx, cy = centers[k] + x0 = max(0, int(cx - rx / 2)) + x1 = min(in_w, int(cx + rx / 2) + 1) + y0 = max(0, int(cy - ry / 2)) + y1 = min(in_h, int(cy + ry / 2) + 1) + patch = nlab_image[y0:y1, x0:x1] + mask_patch = alpha_mask[y0:y1, x0:x1] + if mask_patch.any(): + nu[k] = patch[mask_patch].mean(axis=0) + + # Color variance (scalar), matching paper init + sigma_c = np.full(K, 0.0001, dtype=np.float64) + + # Precompute R(k): set of input pixel indices within 2*rx of each kernel + R = [None] * K + for k in range(K): + cx, cy = centers[k] + x0 = max(0, int(cx - 2 * rx)) + x1 = min(in_w, int(cx + 2 * rx)) + y0 = max(0, int(cy - 2 * ry)) + y1 = min(in_h, int(cy + 2 * ry)) + yy, xx = np.mgrid[y0:y1, x0:x1] + R[k] = (yy.ravel() * in_w + xx.ravel()).astype(np.int32) + + # Eigenvalue clamp bounds, scaled with downscale ratio + # For rx=2 matches paper's [0.05, 0.1]; scales quadratically + ev_min = 0.0125 * rx * rx + ev_max = 0.025 * rx * rx + + # --- EM iterations --- + prev_nu = None + + for it in range(n_iters): + # ===================== E-STEP ===================== + # Compute bilateral weights w_k(i) and per-pixel sums for normalization + + # Per-pixel accumulator: sum of w_k(i) across all kernels k + pixel_wsum = np.zeros(N, dtype=np.float64) + + # Store per-kernel weights (sparse: only pixels in R(k)) + kernel_w = [None] * K + + for k in range(K): + idx = R[k] + pi = coords_flat[idx] # (M, 2) + ci = nlab_flat[idx] # (M, 3) + ai = alpha_flat[idx] # (M,) bool + + # Spatial Gaussian: Mahalanobis distance + diff_s = pi - mu[k] + Si = _invert_2x2(Sigma[k]) + mahal = np.sum(diff_s @ Si * diff_s, axis=-1) + f_k = np.exp(-0.5 * mahal) + + # Color Gaussian + diff_c = ci - nu[k] + color_dist_sq = np.sum(diff_c ** 2, axis=-1) + sc2 = max(sigma_c[k] * sigma_c[k], 1e-15) + g_k = np.exp(-color_dist_sq / (2.0 * sc2)) + + # Bilateral weight + w = f_k * g_k + w[~ai] = 0.0 + + # Per-kernel normalization (numerical stability) + wsum = w.sum() + if wsum > 0: + w /= wsum + + kernel_w[k] = w + pixel_wsum[idx] += w + + # ===================== COMPUTE GAMMA & M-STEP ===================== + # gamma_k(i) = w_k(i) / sum_n w_n(i) [per-pixel normalization] + + new_mu = np.zeros((K, 2), dtype=np.float64) + new_Sigma = np.zeros((K, 2, 2), dtype=np.float64) + new_nu = np.zeros((K, 3), dtype=np.float64) + + # Also store gamma for shape constraint overlap check + kernel_gamma = [None] * K + + for k in range(K): + idx = R[k] + w = kernel_w[k] + pi = coords_flat[idx] + ci = nlab_flat[idx] + + # Per-pixel normalization + denom = pixel_wsum[idx] + denom = np.where(denom > 0, denom, 1.0) + gamma = w / denom + + kernel_gamma[k] = gamma + + gamma_sum = gamma.sum() + if gamma_sum < 1e-12: + new_mu[k] = centers[k] + new_Sigma[k] = Sigma[k] + new_nu[k] = nu[k] + continue + + # M-step + new_mu[k] = (gamma[:, None] * pi).sum(axis=0) / gamma_sum + diff_s = pi - new_mu[k] + new_Sigma[k] = (gamma[:, None, None] * + (diff_s[:, :, None] * diff_s[:, None, :])).sum(axis=0) / gamma_sum + new_nu[k] = (gamma[:, None] * ci).sum(axis=0) / gamma_sum + + mu = new_mu + Sigma = new_Sigma + nu = new_nu + + # ===================== CORRECTION STEP ===================== + + # 1. Spatial bias: blend mu toward 4-neighbor average, clamp to box + mu_grid = mu.reshape(out_h, out_w, 2) + neighbor_sum = np.zeros_like(mu_grid) + neighbor_cnt = np.zeros((out_h, out_w, 1), dtype=np.float64) + + neighbor_sum[1:, :] += mu_grid[:-1, :] + neighbor_cnt[1:, :] += 1 + neighbor_sum[:-1, :] += mu_grid[1:, :] + neighbor_cnt[:-1, :] += 1 + neighbor_sum[:, 1:] += mu_grid[:, :-1] + neighbor_cnt[:, 1:] += 1 + neighbor_sum[:, :-1] += mu_grid[:, 1:] + neighbor_cnt[:, :-1] += 1 + + neighbor_cnt = np.maximum(neighbor_cnt, 1) + mu_bar = neighbor_sum / neighbor_cnt + + mu_grid = 0.5 * mu_grid + 0.5 * mu_bar + + centers_grid = centers.reshape(out_h, out_w, 2) + mu_grid[..., 0] = np.clip(mu_grid[..., 0], + centers_grid[..., 0] - rx / 4.0, + centers_grid[..., 0] + rx / 4.0) + mu_grid[..., 1] = np.clip(mu_grid[..., 1], + centers_grid[..., 1] - ry / 4.0, + centers_grid[..., 1] + ry / 4.0) + mu = mu_grid.reshape(K, 2) + + # 2. Constrain spatial covariance via SVD eigenvalue clamping + for k in range(K): + U, s, Vt = np.linalg.svd(Sigma[k]) + s = np.clip(s, ev_min, ev_max) + Sigma[k] = U @ np.diag(s) @ Vt + + # 3. Shape constraint: check directional variance AND kernel overlap + ky_all = np.arange(K) // out_w + kx_all = np.arange(K) % out_w + + for k in range(K): + ky, kx = ky_all[k], kx_all[k] + gamma_k = kernel_gamma[k] + idx_k = R[k] + + for dy in range(-1, 2): + for dx in range(-1, 2): + if dy == 0 and dx == 0: + continue + ny, nx = ky + dy, kx + dx + if not (0 <= ny < out_h and 0 <= nx < out_w): + continue + nk = ny * out_w + nx + + # Directional variance check + d = mu[nk] - mu[k] + d_norm = np.linalg.norm(d) + if d_norm > 1e-8: + d_hat = d / d_norm + # Weighted directional variance + pi = coords_flat[idx_k] + proj = np.maximum(0, (pi - mu[k]) @ d_hat) + s = (gamma_k * proj * proj).sum() + else: + s = 0.0 + + # Kernel overlap check + # Find common pixels between R(k) and R(nk) + idx_n = R[nk] + gamma_n = kernel_gamma[nk] + common, k_pos, n_pos = np.intersect1d(idx_k, idx_n, + return_indices=True) + if len(common) > 0: + f = (gamma_k[k_pos] * gamma_n[n_pos]).sum() + else: + f = 0.0 + + if s > 0.2 * rx or f < 0.08: + sigma_c[k] *= 1.1 + sigma_c[nk] *= 1.1 + + # Convergence check (after 30 iterations, check color stability) + if prev_nu is not None and it >= 30: + max_delta = np.max(np.abs(nu - prev_nu)) + if verbose: + print(f" iter {it+1}/{n_iters}: max color delta = {max_delta:.6f}") + if max_delta < 0.002: + if verbose: + print(f" Converged at iteration {it+1}") + break + elif verbose: + mu_shift = np.linalg.norm(mu - centers, axis=-1).mean() + print(f" iter {it+1}/{n_iters}: mean spatial shift = {mu_shift:.4f} px") + + prev_nu = nu.copy() + + # --- Build output --- + output_nlab = nu.reshape(out_h, out_w, 3) + + # Transparency: check opaque fraction per cell + output_alpha = np.ones((out_h, out_w), dtype=bool) + for k in range(K): + ky, kx = ky_all[k], kx_all[k] + cx, cy = centers[k] + x0 = max(0, int(cx - rx / 2)) + x1 = min(in_w, int(cx + rx / 2) + 1) + y0 = max(0, int(cy - ry / 2)) + y1 = min(in_h, int(cy + ry / 2) + 1) + patch_alpha = alpha_mask[y0:y1, x0:x1] + total = patch_alpha.size + opaque = patch_alpha.sum() + if total > 0 and (opaque / total) < 0.3: + output_alpha[ky, kx] = False + + return output_nlab, output_alpha + + +# --------------------------------------------------------------------------- +# Palette assignment +# --------------------------------------------------------------------------- + +def assign_to_palette(output_nlab, output_alpha, palette_rgb): + """Assign each output pixel to the nearest color in the provided palette. + + Matches in CIELAB space. Palette is used exactly as-is. + """ + H, W = output_nlab.shape[:2] + + # Convert palette to normalized CIELAB for comparison + pal_rgb01 = palette_rgb.astype(np.float64) / 255.0 + pal_lab = rgb_to_lab(pal_rgb01.reshape(1, -1, 3)).reshape(16, 3) + pal_nlab = normalize_lab(pal_lab) + + # Denormalize output for comparison in same space + indices = np.zeros((H, W), dtype=np.uint8) + for y in range(H): + for x in range(W): + if not output_alpha[y, x]: + indices[y, x] = 0 + else: + d = np.sum((output_nlab[y, x] - pal_nlab) ** 2, axis=-1) + indices[y, x] = np.argmin(d) + + return indices + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def parse_args(argv=None): + p = argparse.ArgumentParser( + description="Content-adaptive sprite downscaler for Genesis pixel art.", + ) + p.add_argument("input", help="Input PNG path") + p.add_argument("output", help="Output indexed PNG path") + p.add_argument("--size", required=True, + help="Output size WxH (multiples of 8)") + p.add_argument("--palette", required=True, + help="Indexed PNG to use as the 16-color palette") + p.add_argument("--iters", type=int, default=50, + help="Max EM iterations (default 50, converges early)") + p.add_argument("--bg-color", + help="Treat this R,G,B color as transparent (for RGB inputs)") + p.add_argument("--seed", type=int, default=None, + help="RNG seed for reproducibility") + p.add_argument("--verbose", action="store_true", + help="Print per-iteration convergence stats") + return p.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + + # Parse size + try: + w_str, h_str = args.size.lower().split("x") + out_w, out_h = int(w_str), int(h_str) + except ValueError: + print(f"Error: --size must be WxH, got '{args.size}'", file=sys.stderr) + sys.exit(1) + + if out_w % 8 != 0 or out_h % 8 != 0: + print("Error: output dimensions must be multiples of 8", file=sys.stderr) + sys.exit(1) + + # Load palette + palette_path = Path(args.palette) + if not palette_path.exists(): + print(f"Error: palette file not found: {args.palette}", file=sys.stderr) + sys.exit(1) + ext_palette = load_palette(args.palette) + + # Parse bg color + bg_color = None + if args.bg_color: + try: + parts = [int(x.strip()) for x in args.bg_color.split(",")] + if len(parts) != 3: + raise ValueError + bg_color = tuple(parts) + except ValueError: + print("Error: --bg-color must be R,G,B", file=sys.stderr) + sys.exit(1) + + # Load input + input_path = Path(args.input) + if not input_path.exists(): + print(f"Error: input file not found: {args.input}", file=sys.stderr) + sys.exit(1) + + print(f"Loading {args.input}...") + nlab_image, alpha_mask = load_input(args.input, bg_color) + in_h, in_w = nlab_image.shape[:2] + + if out_w >= in_w or out_h >= in_h: + print(f"Error: output ({out_w}x{out_h}) must be smaller than input ({in_w}x{in_h})", + file=sys.stderr) + sys.exit(1) + + print(f"Input: {in_w}x{in_h}") + print(f"Output: {out_w}x{out_h}") + print(f"Palette: {args.palette}") + print(f"Max EM iterations: {args.iters}") + + # Run EM downscale + print("Running EM downscale...") + output_nlab, output_alpha = run_downscale( + nlab_image, alpha_mask, out_h, out_w, + n_iters=args.iters, verbose=args.verbose, + ) + + # Assign to provided palette + print("Assigning to palette...") + indices = assign_to_palette(output_nlab, output_alpha, ext_palette) + + # Save + print(f"Saving {args.output}...") + save_indexed_png(args.output, indices, ext_palette) + + # Summary + n_opaque = output_alpha.sum() + n_transparent = (~output_alpha).sum() + colors_used = len(np.unique(indices[output_alpha])) if n_opaque > 0 else 0 + print(f"\nDone!") + print(f" Opaque pixels: {n_opaque}") + print(f" Transparent pixels: {n_transparent}") + print(f" Colors used: {colors_used} / 15") + + +if __name__ == "__main__": + main() diff --git a/tools/suika.png b/tools/suika.png new file mode 100644 index 0000000000000000000000000000000000000000..f80cb2240597c8fcd23997ec5043b5b1f91c413c GIT binary patch literal 5610 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 literal 0 HcmV?d00001 diff --git a/tools/suikasm.png b/tools/suikasm.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3f1f1391cc07c253632045cfa9029ddcd11430 GIT binary patch literal 2442 zcmeHIZA=?w96w-8C@`IK88Vj~M=+qY_tMhRgKjWd20|;N-NZM>ard-6Xz$LuD{a}x z6vpCk`{3K0&Jmf5)5)Ab-T3X~V1A(f3ov%*z|qSy7a;z|bt3}*n+TM?lX@2< zlPT5SuOLX)675bwUU)GVK^9!*J#Cpn>7UHeiSZr5N>!RS}6{JAjI|0Us~e zupi$207H4shTT`{B%D$;@bk4@GH`dfJZx77Yvr(2E76Jw4HX1|%Ak>8Kv3w24U6fe z;adBQV`vPbcG$2g%^=$BY(%R?8K5L#AoMIj5U9m!;3xu^%w{7|f>H!kijx#hSoH)& zlO#o9s1ldvIsN{HiL^I35m%4Pm1}lzlj9wOVn4!YN7*5qc#ms7ypJ zCI78;P!#0i%ktM-&5zWHlySMk$M-6fJTxEH7vzisy*{JEH;{2J>dt z6DB=r^pIxSWT7b%ZfSy;gE~cy_eTE=wa_Lb4aMd|Vc~F$%KRgkV`;A_2N_724>CT0 zOM(wWlYykG#egV7!6ci5K$F*LZ zV}k#YWbABS-zMO27--AnB!ZjdECT`@U^1LkXU9Xk5F`b!vsZb_c0B8TLKr2oE579Y zS>Ii~sytX6opiUpmAQaFe4xm-=kxPx9^JfU|Dws(7V5)1f5!6Zysn)k$NSz+x3p(R zUGBScD?97+e$n|?gffQ5a!cMX$bCGsf2zC(-@JbNdj*vT(VFfaV{h-Jmo6>3XWi(w zrTwRd@-}vt`4lvF?}qU5rB(UMmTbSUYth6&>XeRa>Kj^j?qKSkWk0HqEj~Zf{?xBW z5W4nOI(x$P{Krivj&0$O`)p62d42cEz_F3VJHh%*Bf2}%$TQ_R^~CN6ZeIP&;WsCW z2ZvTv4(Gp`23(&#x%H#;uT!3DUsyOc#V(vaY15y6S(ml{6#D6F1EGiS9NtvDujQ5U zD@P*sUU7D0EjnFo9Y~*Lim`^D3qSE*oON`sojg6$^8JQ$H{Eto*vl6F^pN(8sB^gN JZ>( literal 0 HcmV?d00001