diff --git a/res/boss.vgm b/res/boss.vgm new file mode 100644 index 0000000..2a15168 Binary files /dev/null and b/res/boss.vgm differ diff --git a/res/fairy.png b/res/fairy.png deleted file mode 100644 index 34f9b8e..0000000 Binary files a/res/fairy.png and /dev/null differ diff --git a/res/font.png b/res/font.png index ba87fa7..fe18dd4 100644 Binary files a/res/font.png and b/res/font.png differ diff --git a/res/ground.png b/res/ground.png index ef08f9c..c57aed4 100644 Binary files a/res/ground.png and b/res/ground.png differ diff --git a/res/koakuma.png b/res/koakuma.png deleted file mode 100644 index aef3941..0000000 Binary files a/res/koakuma.png and /dev/null differ diff --git a/res/level.vgm b/res/level.vgm new file mode 100644 index 0000000..3f04ee4 Binary files /dev/null and b/res/level.vgm differ diff --git a/res/logo.png b/res/logo.png deleted file mode 100644 index e1d2f5a..0000000 Binary files a/res/logo.png and /dev/null differ diff --git a/res/mapenemy.png b/res/mapenemy.png deleted file mode 100644 index b14a824..0000000 Binary files a/res/mapenemy.png and /dev/null differ diff --git a/res/mapindicator.png b/res/mapindicator.png index bff5d3a..5b9d55d 100644 Binary files a/res/mapindicator.png and b/res/mapindicator.png differ diff --git a/res/mapplayer.png b/res/mapplayer.png deleted file mode 100644 index c08bfcf..0000000 Binary files a/res/mapplayer.png and /dev/null differ diff --git a/res/momoyo.png b/res/momoyo.png new file mode 100644 index 0000000..50e360c Binary files /dev/null and b/res/momoyo.png differ diff --git a/res/pbullet.png b/res/pbullet.png index d1b92a7..37b0267 100644 Binary files a/res/pbullet.png and b/res/pbullet.png differ diff --git a/res/resources.res b/res/resources.res index 315ddf7..a8b3413 100644 --- a/res/resources.res +++ b/res/resources.res @@ -1,28 +1,33 @@ IMAGE font "font.png" NONE NONE IMAGE shadow "shadow.png" NONE NONE -IMAGE logo "logo.png" NONE NONE +IMAGE startFade1 "start/fade1.png" FAST +IMAGE startFade2 "start/fade2.png" FAST +IMAGE startFade3 "start/fade3.png" FAST +IMAGE startFade4 "start/fade4.png" FAST +IMAGE startSplash1 "start/splash1.png" FAST +IMAGE startLogo "start/logo.png" FAST +IMAGE startBigBg "start/bigbg.png" FAST +XGM2 bgmStart "start.vgm" IMAGE sky "sky.png" NONE NONE +IMAGE skyTop "skytop.png" NONE NONE +IMAGE skyRed "skyred.png" NONE NONE IMAGE ground "ground.png" NONE NONE -// SPRITE sakuyaSprite "sakuya.png" 4 4 NONE 0 -SPRITE sakuyaSprite "sakuya2.png" 6 6 NONE 0 +SPRITE momoyoSprite "momoyo.png" 6 6 NONE 0 SPRITE bulletsSprite "bullets.png" 2 2 NONE 0 SPRITE pBulletSprite "pbullet.png" 4 4 NONE 0 -// SPRITE fairySprite "fairy.png" 4 4 NONE 8 SPRITE fairySprite "fairy2.png" 4 4 NONE 8 -SPRITE koaSprite "koakuma.png" 4 4 NONE 0 +SPRITE treasureSprite "treasure.png" 4 4 NONE 0 -// IMAGE mapPlayer "mapplayer.png" NONE NONE IMAGE mapIndicator "mapindicator.png" NONE NONE -// IMAGE mapFrame "mapframe.png" NONE NONE IMAGE imageFontBig "fontbig.png" NONE NONE IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE IMAGE imageChromeLife "life.png" NONE NONE IMAGE imageChromeLife2 "life2.png" NONE NONE -XGM2 stageMusic "stage.vgm" \ No newline at end of file +XGM2 stageMusic "level.vgm" \ No newline at end of file diff --git a/res/sakuya.png b/res/sakuya.png deleted file mode 100644 index f02273d..0000000 Binary files a/res/sakuya.png and /dev/null differ diff --git a/res/sakuya2.png b/res/sakuya2.png deleted file mode 100644 index e201cca..0000000 Binary files a/res/sakuya2.png and /dev/null differ diff --git a/res/sky.png b/res/sky.png index e119cf2..0e23ae5 100644 Binary files a/res/sky.png and b/res/sky.png differ diff --git a/res/skyred.png b/res/skyred.png new file mode 100644 index 0000000..de2f688 Binary files /dev/null and b/res/skyred.png differ diff --git a/res/mapframe.png b/res/skytop.png similarity index 58% rename from res/mapframe.png rename to res/skytop.png index 80d11a8..a25d59e 100644 Binary files a/res/mapframe.png and b/res/skytop.png differ diff --git a/res/start.vgm b/res/start.vgm new file mode 100644 index 0000000..57b99be Binary files /dev/null and b/res/start.vgm differ diff --git a/res/start/bigbg.png b/res/start/bigbg.png new file mode 100644 index 0000000..1237907 Binary files /dev/null and b/res/start/bigbg.png differ diff --git a/res/start/fade1.png b/res/start/fade1.png new file mode 100644 index 0000000..ed9f3d7 Binary files /dev/null and b/res/start/fade1.png differ diff --git a/res/start/fade2.png b/res/start/fade2.png new file mode 100644 index 0000000..71838d1 Binary files /dev/null and b/res/start/fade2.png differ diff --git a/res/start/fade3.png b/res/start/fade3.png new file mode 100644 index 0000000..9504783 Binary files /dev/null and b/res/start/fade3.png differ diff --git a/res/start/fade4.png b/res/start/fade4.png new file mode 100644 index 0000000..673b73b Binary files /dev/null and b/res/start/fade4.png differ diff --git a/res/start/logo.png b/res/start/logo.png new file mode 100644 index 0000000..05a1ae2 Binary files /dev/null and b/res/start/logo.png differ diff --git a/res/start/splash1.png b/res/start/splash1.png new file mode 100644 index 0000000..57d3765 Binary files /dev/null and b/res/start/splash1.png differ diff --git a/res/treasure.png b/res/treasure.png new file mode 100644 index 0000000..fd8511f Binary files /dev/null and b/res/treasure.png differ diff --git a/res/treasure.vgm b/res/treasure.vgm new file mode 100644 index 0000000..3b4a6bf Binary files /dev/null and b/res/treasure.vgm differ diff --git a/src/background.h b/src/background.h index 4983eed..ab3a23a 100644 --- a/src/background.h +++ b/src/background.h @@ -14,27 +14,49 @@ static const fix32 parallaxMul[PARALLAX_COUNT] = { }; s16 bgScroll[28]; +u8 bgOff; void loadBackground(){ VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE); VDP_setVerticalScroll(BG_B, 32); - VDP_loadTileSet(sky.tileset, BG_I, DMA); - VDP_loadTileSet(ground.tileset, BG_I + 64, DMA); - for(u8 y = 0; y < 4; y++){ + VDP_loadTileSet(skyTop.tileset, BG_I, DMA); + VDP_loadTileSet(sky.tileset, BG_I + 64, DMA); + VDP_loadTileSet(ground.tileset, BG_I + 128, DMA); + VDP_loadTileSet(skyRed.tileset, BG_I + 192, DMA); + + // for(u8 y = 0; y < 14; y++){ + // for(u8 x = 0; x < 64; x++){ + // if(y < 11) bgOff = 0; + // // else if(y == 13) bgOff = 2; + // else bgOff = 1; + // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + bgOff * 2), x * 2, y * 2 + 0, 2, 1); + // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 28 + bgOff * 2), x * 2, y * 2 + 1, 2, 1); + // } + // } + + // for(u8 y = 0; y < 4; y++){ + // for(u8 x = 0; x < 16; x++){ + // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 2), x * 2, y * 2, 2, 1); + // // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 2 + 28), x * 2, y * 2 + 1, 2, 1); + // } + // } + + VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 192), 0, 0, 128, 8); + + for(u8 y = 0; y < 3; y++){ for(u8 x = 0; x < 16; x++){ - VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + (y > 2 ? 64 : 0)), x * 8, y * 8, 8, 8); + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 * y), x * 8, y * 8 + 8, 8, 8); } } + // place 64x64 ground block in sky area (zone 0 only) - VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); + // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); zoneBlockVisible = TRUE; prevCamera = player.camera; for(u8 i = 0; i < PARALLAX_COUNT; i++) parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]); // write initial scroll values so first frame has correct parallax s16 initScroll = fix32ToInt(-player.camera); - for(u8 i = 0; i < 20; i++) - bgScroll[i] = initScroll; for(u8 i = 0; i < 8; i++) bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i])); VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); @@ -68,11 +90,11 @@ void updateBackground(){ // show ground block only when zone 0 copy of these columns is on screen fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160)); bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212)); - if(shouldShow && !zoneBlockVisible){ - VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); - zoneBlockVisible = TRUE; - } else if(!shouldShow && zoneBlockVisible){ - VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); - zoneBlockVisible = FALSE; - } + // if(shouldShow && !zoneBlockVisible){ + // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); + // zoneBlockVisible = TRUE; + // } else if(!shouldShow && zoneBlockVisible){ + // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); + // zoneBlockVisible = FALSE; + // } } diff --git a/src/bullets.h b/src/bullets.h index 5e65a49..7a813c9 100644 --- a/src/bullets.h +++ b/src/bullets.h @@ -57,7 +57,7 @@ static void doBulletRotation(u8 i){ } void spawnBullet(struct bulletSpawner spawner, void(*updater)){ - if(player.recoveringClock > 0 && !spawner.player) return; + if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return; // Don't spawn if offscreen fix32 dx = getWrappedDelta(spawner.x, player.pos.x); bool offScreenX = (dx < -CULL_LIMIT || dx > CULL_LIMIT); @@ -114,7 +114,7 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){ s32 bulletDist; static void collideWithEnemy(u8 i){ for(s16 j = 0; j < ENEMY_COUNT; j++) { - if(enemies[j].active && enemies[j].onScreen && bullets[i].active){ + if(enemies[j].active && bullets[i].active){ fix32 deltaX = getWrappedDelta(bullets[i].pos.x, enemies[j].pos.x); fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y; if(deltaY >= -BULLET_CHECK && deltaY <= BULLET_CHECK && @@ -132,7 +132,7 @@ static void collideWithEnemy(u8 i){ } static void collideWithPlayer(u8 i){ - if(player.recoveringClock > 0) return; + if(player.recoveringClock > 0 || player.respawnClock > 0) return; fix32 deltaX = getWrappedDelta(bullets[i].pos.x, player.pos.x); fix32 deltaY = bullets[i].pos.y - player.pos.y; @@ -152,8 +152,11 @@ static void collideWithPlayer(u8 i){ gameOver = TRUE; XGM2_stop(); } else { - player.recoveringClock = 120; + player.respawnClock = 120; + SPR_setVisibility(player.image, HIDDEN); killBullets = TRUE; + hitMessageClock = 120; + hitMessageBullet = TRUE; } } } diff --git a/src/chrome.h b/src/chrome.h index dc0ad08..25ea989 100644 --- a/src/chrome.h +++ b/src/chrome.h @@ -1,8 +1,8 @@ #define MAP_I 512 -#define MAP_TILE TILE_ATTR_FULL(PAL1, 1, 0, 0, MAP_I) +#define MAP_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I) #define MAP_PLAYER_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 1) #define MAP_ENEMY_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 2) -#define MAP_HUMAN_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3) +#define MAP_TREASURE_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3) #define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4) #define FONT_BIG_I 256 @@ -37,7 +37,7 @@ static void drawLives(){ // previous map positions: -1 means not drawn s16 mapEnemyCol[ENEMY_COUNT], mapEnemyRow[ENEMY_COUNT]; -s16 mapHumanCol[HUMAN_COUNT], mapHumanRow[HUMAN_COUNT]; +s16 mapTreasureCol[TREASURE_COUNT], mapTreasureRow[TREASURE_COUNT]; s16 mapPlayerRow; static void drawScore(){ @@ -57,33 +57,40 @@ static void drawScore(){ void loadMap(){ VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H); - // VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3), MAP_X, MAP_Y - 1, MAP_W, 1); - // VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 1, 0, MAP_I + 3), MAP_X, MAP_Y + MAP_H, MAP_W, 1); + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1); + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1); - // VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4), MAP_X - 1, MAP_Y, 1, MAP_H); + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H); + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H); + + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1); + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1); + + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1); + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1); for(s16 i = 0; i < ENEMY_COUNT; i++){ mapEnemyCol[i] = -1; mapEnemyRow[i] = -1; } - for(s16 i = 0; i < HUMAN_COUNT; i++){ - mapHumanCol[i] = -1; - mapHumanRow[i] = -1; + for(s16 i = 0; i < TREASURE_COUNT; i++){ + mapTreasureCol[i] = -1; + mapTreasureRow[i] = -1; } mapPlayerRow = -1; } // temp arrays for new positions s16 mapNewCol[ENEMY_COUNT], mapNewRow[ENEMY_COUNT]; -s16 mapNewHumanCol[HUMAN_COUNT], mapNewHumanRow[HUMAN_COUNT]; +s16 mapNewTreasureCol[TREASURE_COUNT], mapNewTreasureRow[TREASURE_COUNT]; static bool mapTileOccupied(s16 col, s16 row, s16 pRow){ // player always at center column if(col == MAP_W / 2 && row == pRow) return TRUE; for(s16 i = 0; i < ENEMY_COUNT; i++) if(mapNewCol[i] == col && mapNewRow[i] == row) return TRUE; - for(s16 i = 0; i < HUMAN_COUNT; i++) - if(mapNewHumanCol[i] == col && mapNewHumanRow[i] == row) return TRUE; + for(s16 i = 0; i < TREASURE_COUNT; i++) + if(mapNewTreasureCol[i] == col && mapNewTreasureRow[i] == row) return TRUE; return FALSE; } @@ -112,22 +119,22 @@ static void updateMap(){ mapNewRow[i] = row; } - // compute new human positions - for(s16 i = 0; i < HUMAN_COUNT; i++){ - if(!humans[i].active || humans[i].image == NULL || humans[i].state == HUMAN_COLLECTED){ - mapNewHumanCol[i] = -1; - mapNewHumanRow[i] = -1; + // compute new treasure positions + for(s16 i = 0; i < TREASURE_COUNT; i++){ + if(!treasures[i].active || treasures[i].image == NULL || treasures[i].state == TREASURE_COLLECTED){ + mapNewTreasureCol[i] = -1; + mapNewTreasureRow[i] = -1; continue; } - fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x); + fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x); s16 col = fix32ToInt(dx) / 54 + MAP_W / 2; if(col < 0) col = 0; if(col >= MAP_W) col = MAP_W - 1; - s16 row = fix32ToInt(humans[i].pos.y) / 75; + s16 row = fix32ToInt(treasures[i].pos.y) / 75; if(row < 0) row = 0; if(row >= MAP_H) row = MAP_H - 1; - mapNewHumanCol[i] = col; - mapNewHumanRow[i] = row; + mapNewTreasureCol[i] = col; + mapNewTreasureRow[i] = row; } // clear old player tile if it moved and nothing new occupies it @@ -143,21 +150,21 @@ static void updateMap(){ VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapEnemyCol[i], MAP_Y + mapEnemyRow[i]); } - // clear old human tiles that moved or disappeared - for(s16 i = 0; i < HUMAN_COUNT; i++){ - if(mapHumanCol[i] < 0) continue; - if(mapHumanCol[i] == mapNewHumanCol[i] && mapHumanRow[i] == mapNewHumanRow[i]) continue; - if(!mapTileOccupied(mapHumanCol[i], mapHumanRow[i], pRow)) - VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapHumanCol[i], MAP_Y + mapHumanRow[i]); + // clear old treasure tiles that moved or disappeared + for(s16 i = 0; i < TREASURE_COUNT; i++){ + if(mapTreasureCol[i] < 0) continue; + if(mapTreasureCol[i] == mapNewTreasureCol[i] && mapTreasureRow[i] == mapNewTreasureRow[i]) continue; + if(!mapTileOccupied(mapTreasureCol[i], mapTreasureRow[i], pRow)) + VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapTreasureCol[i], MAP_Y + mapTreasureRow[i]); } - // draw human dots (skip if player occupies same tile) - for(s16 i = 0; i < HUMAN_COUNT; i++){ - mapHumanCol[i] = mapNewHumanCol[i]; - mapHumanRow[i] = mapNewHumanRow[i]; - if(mapNewHumanCol[i] < 0) continue; - if(mapNewHumanCol[i] == MAP_W / 2 && mapNewHumanRow[i] == pRow) continue; - VDP_setTileMapXY(BG_A, MAP_HUMAN_TILE, MAP_X + mapNewHumanCol[i], MAP_Y + mapNewHumanRow[i]); + // draw treasure dots (skip if player occupies same tile) + for(s16 i = 0; i < TREASURE_COUNT; i++){ + mapTreasureCol[i] = mapNewTreasureCol[i]; + mapTreasureRow[i] = mapNewTreasureRow[i]; + if(mapNewTreasureCol[i] < 0) continue; + if(mapNewTreasureCol[i] == MAP_W / 2 && mapNewTreasureRow[i] == pRow) continue; + VDP_setTileMapXY(BG_A, MAP_TREASURE_TILE, MAP_X + mapNewTreasureCol[i], MAP_Y + mapNewTreasureRow[i]); } // draw enemy dots (skip if player occupies same tile) @@ -174,12 +181,14 @@ static void updateMap(){ VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow); } +u8 phraseIndex[4]; + s16 lastLevel; static void drawLevel(){ char lvlStr[4]; uintToStr(level + 1, lvlStr, 1); - VDP_drawText("L", 1, 26); - VDP_drawText(lvlStr, 2, 26); + VDP_drawText("LVL", 1, 8); + VDP_drawText(lvlStr, 4, 8); lastLevel = level; } @@ -201,34 +210,36 @@ static void doGameOver(){ didGameOver = TRUE; 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 < HUMAN_COUNT; i++) if(humans[i].active){ - if(humans[i].state == HUMAN_COLLECTED){ - // spawn player bullet explosion at carried human position + for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active){ + if(treasures[i].state == TREASURE_COLLECTED){ + // spawn player bullet explosion at carried treasure position struct bulletSpawner spawner = { - .x = humans[i].pos.x, .y = humans[i].pos.y, + .x = treasures[i].pos.x, .y = treasures[i].pos.y, .anim = 0, .speed = 0, .angle = 0, .player = TRUE }; 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 - && bullets[j].pos.x == humans[i].pos.x && bullets[j].pos.y == humans[i].pos.y){ + && bullets[j].pos.x == treasures[i].pos.x && bullets[j].pos.y == treasures[i].pos.y){ killBullet(j, TRUE); break; } } } - killHuman(i); + killTreasure(i); } SPR_releaseSprite(player.image); - // clear minimap - VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H); - // clear score - VDP_clearTileMapRect(BG_A, SCORE_X, SCORE_Y, SCORE_LENGTH, 2); // clear lives VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16); + // clear messages + treasureCollectedClock = 0; + allTreasureCollected = FALSE; + hitMessageClock = 0; + VDP_clearText(9, 5, 22); + VDP_drawText("GAME OVER", 15, 13); VDP_drawText("PRESS ANY BUTTON", 12, 14); } @@ -236,7 +247,7 @@ static void doGameOver(){ 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 < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1); + for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL1); SPR_setPalette(player.image, PAL1); XGM2_pause(); VDP_drawText("PAUSE", 17, 13); @@ -245,7 +256,7 @@ static void showPause(){ 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 < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL0); + for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0); SPR_setPalette(player.image, PAL0); XGM2_resume(); VDP_clearText(17, 13, 5); @@ -307,5 +318,52 @@ void updateChrome(){ } if(lastLives != player.lives) drawLives(); if(lastLevel != level) drawLevel(); + if(treasureCollectedClock > 0 && levelWaitClock == 0){ + if(treasureCollectedClock == 120){ + VDP_clearText(10, 5, 22); + const char* mirrorPhrases[] = {"REFLECT THE DEPTHS", "DIG DEEPER WITHIN", "SEE WHAT SHINES BELOW", "MIRROR OF THE MINE", "LOOK BACK STRIKE BACK"}; + const char* lampPhrases[] = {"STRIKE LIGHT", "LET THERE BE LODE", "BRIGHT IDEA DEEP DOWN", "ILLUMINATE THE VEIN", "GLOW FROM BELOW"}; + const char* scarfPhrases[] = {"COZY IN THE CAVES", "WRAP THE UNDERWORLD", "SNUG AS BEDROCK", "STYLE FROM THE STRATA", "WARM THE DEPTHS"}; + const char* swordPhrases[] = {"ORE YOU READY", "MINED YOUR STEP", "CUTTING EDGE GEOLOGY", "STRIKE THE VEIN", "SPIRIT STEEL"}; + const char** sets[] = {mirrorPhrases, lampPhrases, scarfPhrases, swordPhrases}; + const char* phrase = sets[treasureCollectedType][phraseIndex[treasureCollectedType]]; + phraseIndex[treasureCollectedType] = (phraseIndex[treasureCollectedType] + 1) % 5; + u8 len = strlen(phrase); + VDP_drawText(phrase, 20 - len / 2, 5); + } + treasureCollectedClock--; + if(treasureCollectedClock == 0){ + VDP_clearText(10, 5, 22); + // check if all treasures are collected or gone + bool allDone = TRUE; + for(s16 j = 0; j < TREASURE_COUNT; j++){ + if(treasures[j].active && treasures[j].state != TREASURE_COLLECTED){ + allDone = FALSE; + break; + } + } + if(allDone && collectedCount > 0){ + allTreasureCollected = TRUE; + VDP_drawText("ALL TREASURE COLLECTED", 9, 5); + } + } + } + if(hitMessageClock > 0){ + if(hitMessageClock == 120){ + VDP_clearText(9, 5, 22); + treasureCollectedClock = 0; + allTreasureCollected = FALSE; + VDP_drawText(hitMessageBullet ? "BLASTED" : "SMASHED", hitMessageBullet ? 16 : 16, 5); + } + hitMessageClock--; + if(hitMessageClock == 0) + VDP_clearText(9, 5, 22); + } + if(levelWaitClock == 240){ + VDP_clearText(9, 5, 22); + treasureCollectedClock = 0; + allTreasureCollected = FALSE; + VDP_drawText("ALL ENEMIES DESTROYED", 9, 5); + } if(clock % 4 == 0) updateMap(); } \ No newline at end of file diff --git a/src/enemies.h b/src/enemies.h index 7fc269d..8e7b3b5 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -83,18 +83,18 @@ 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 the human was collected by player or gone, kill this enemy - if(!humans[h].active || humans[h].state == HUMAN_COLLECTED){ + // 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; - humanBeingCarried = FALSE; + treasureBeingCarried = FALSE; killEnemy(i); return; } // carrying: only check for reaching the top else if(enemies[i].pos.y <= FIX32(0)){ - if(humans[h].active) killHuman(h); + if(treasures[h].active) killTreasure(h); enemies[i].ints[3] = -1; - humanBeingCarried = FALSE; + treasureBeingCarried = FALSE; if(enemies[i].type == ENEMY_TYPE_BUILDER){ u8 zone = fix32ToInt(enemies[i].pos.x) / 512; spawnEnemy(ENEMY_TYPE_GUNNER, zone); @@ -149,11 +149,33 @@ static void updateEnemy(u8 i){ } // enemy->player collision - if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0){ + if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0 && player.respawnClock == 0){ fix32 edx = getWrappedDelta(enemies[i].pos.x, player.pos.x); 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_setAnim(bullets[expSlot].image, 1); + SPR_setFrame(bullets[expSlot].image, 0); + SPR_setHFlip(bullets[expSlot].image, random() & 1); + } else { + bullets[expSlot].active = FALSE; + } + } if(enemies[i].type != ENEMY_TYPE_BOSS){ enemies[i].hp = 0; killEnemy(i); @@ -163,8 +185,11 @@ static void updateEnemy(u8 i){ gameOver = TRUE; XGM2_stop(); } else { - player.recoveringClock = 120; + player.respawnClock = 120; + SPR_setVisibility(player.image, HIDDEN); killBullets = TRUE; + hitMessageClock = 120; + hitMessageBullet = FALSE; } } } diff --git a/src/enemytypes.h b/src/enemytypes.h index a020b60..e5078bc 100644 --- a/src/enemytypes.h +++ b/src/enemytypes.h @@ -2,29 +2,29 @@ // --- Type 0: Test / Fairy (EnemyOne) --- // ============================================================================= // The original enemy type. A fairy that shoots 8-bullet circular bursts and -// also seeks/abducts humans (same carry behavior as Builder). +// also seeks/abducts treasures (same carry behavior as Builder). // // Behavior: // - When NOT carrying: drifts at speed 2, shoots 8 bullets in a circle // every 20 frames (only when on screen). Also scans for nearby walking -// humans every 30 frames and steers toward the closest one within 256px. -// Grabs the human when within 16px. +// treasures every 30 frames and steers toward the closest one within 256px. +// Grabs the treasure when within 16px. // - When carrying: flies upward (angle 704-832, roughly up-left to up-right) // at speed 2. Skips all shooting. boundsEnemy() handles reaching the top -// (kills human, self-destructs -- does NOT spawn a Gunner unlike Builder). +// (kills treasure, self-destructs -- does NOT spawn a Gunner unlike Builder). // // ints[0] = random shot timer offset (0-59), desynchronizes shooting from // other enemies so they don't all fire on the same frame -// ints[2] = target human index (-1 = no target) -// ints[3] = carried human index (-1 = not carrying) +// ints[2] = target treasure index (-1 = no target) +// ints[3] = carried treasure index (-1 = not carrying) // // Speed: 2 HP: 1 Shoots: yes (8-bullet radial, every 20 frames) -// Abducts: yes (only 1 human globally at a time via humanBeingCarried flag) +// Abducts: yes (only 1 treasure globally at a time via treasureBeingCarried flag) // ============================================================================= void loadEnemyOne(u8 i){ enemies[i].ints[0] = random() % 60; - enemies[i].ints[2] = -1; // target human index - enemies[i].ints[3] = -1; // carried human index + enemies[i].ints[2] = -1; // target treasure index + enemies[i].ints[3] = -1; // carried treasure index enemies[i].angle = ((random() % 4) * 256) + 128; enemies[i].speed = FIX32(2); } @@ -39,41 +39,41 @@ void updateEnemyOne(u8 i){ return; } - // cancel any target if a human is already being carried - if(humanBeingCarried && enemies[i].ints[2] >= 0){ + // 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 human to grab - if(!humanBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){ - s16 bestHuman = -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 < HUMAN_COUNT; j++){ - if(!humans[j].active || humans[j].state != HUMAN_WALKING) continue; - fix32 dx = getWrappedDelta(enemies[i].pos.x, humans[j].pos.x); - fix32 dy = enemies[i].pos.y - humans[j].pos.y; + 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; - bestHuman = j; + bestTreasure = j; } } - enemies[i].ints[2] = bestHuman; + enemies[i].ints[2] = bestTreasure; } - // steer toward target human + // steer toward target treasure if(enemies[i].ints[2] >= 0){ s16 t = enemies[i].ints[2]; - if(!humans[t].active || humans[t].state != HUMAN_WALKING){ + if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){ enemies[i].ints[2] = -1; } else { - fix32 dx = getWrappedDelta(humans[t].pos.x, enemies[i].pos.x); - fix32 dy = humans[t].pos.y - enemies[i].pos.y; + fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x); + fix32 dy = treasures[t].pos.y - enemies[i].pos.y; - // hone toward human's current position at base speed + // hone toward treasure's current position at base speed s16 angle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(humans[t].pos.x), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(humans[t].pos.y)); + fix32ToFix16(enemies[i].pos.x), fix32ToFix16(treasures[t].pos.x), + fix32ToFix16(enemies[i].pos.y), fix32ToFix16(treasures[t].pos.y)); enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed); enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed); @@ -83,9 +83,9 @@ void updateEnemyOne(u8 i){ if(adx < FIX32(16) && ady < FIX32(16)){ enemies[i].ints[3] = t; enemies[i].ints[2] = -1; - humanBeingCarried = TRUE; - humans[t].state = HUMAN_CARRIED; - humans[t].carriedBy = i; + treasureBeingCarried = TRUE; + treasures[t].state = TREASURE_CARRIED; + treasures[t].carriedBy = i; return; } } @@ -150,6 +150,7 @@ void updateDrone(u8 i){ enemies[i].angle = honeAngle( fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + if(player.respawnClock > 0) enemies[i].angle = (enemies[i].angle + 512) % 1024; enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); } @@ -162,6 +163,7 @@ void updateDrone(u8 i){ s16 aimAngle = honeAngle( fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, @@ -236,6 +238,7 @@ void updateGunner(u8 i){ s16 aimAngle = honeAngle( fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, @@ -256,12 +259,12 @@ void updateGunner(u8 i){ // ============================================================================= // 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 focus mode / diagonal movement to +// 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, focus = 3.5). +// - Moves at speed 5 (player normal speed = 6). // - Bounces off top/bottom screen edges. // - No shooting, no abduction. Just chases. // @@ -285,6 +288,7 @@ void updateHunter(u8 i){ enemies[i].angle = honeAngle( fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + if(player.respawnClock > 0) enemies[i].angle = (enemies[i].angle + 512) % 1024; enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); } @@ -292,30 +296,30 @@ void updateHunter(u8 i){ // ============================================================================= // --- Type 4: Builder (Abductor) --- // ============================================================================= -// Human abductor. Drifts slowly, scans for walking humans, grabs one, and -// flies upward. If it reaches the top of the screen with a human, the human +// 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 human can be globally carried at a time -// (humanBeingCarried flag). +// 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 human +// - 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 human, spawns a Gunner +// boundsEnemy() handles reaching the top: kills the treasure, spawns a Gunner // at the builder's position, then self-destructs. -// - If the carried human gets collected by the player while being carried, -// boundsEnemy() detects this and kills the builder (enemy dies, human safe). -// - Cancels its target if another enemy is already carrying a human. +// - 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 human index (-1 = no target) -// ints[3] = carried human index (-1 = not carrying) +// 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 human + spawns Gunner if it reaches the top) +// Abducts: yes (kills treasure + spawns Gunner if it reaches the top) // // Design notes: // - 1 builder adds light urgency. 2 builders is stressful. @@ -339,40 +343,40 @@ void updateBuilder(u8 i){ return; } - // cancel target if a human is already being carried - if(humanBeingCarried && enemies[i].ints[2] >= 0){ + // 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 human every 30 frames - if(!humanBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){ - s16 bestHuman = -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 < HUMAN_COUNT; j++){ - if(!humans[j].active || humans[j].state != HUMAN_WALKING) continue; - fix32 dx = getWrappedDelta(enemies[i].pos.x, humans[j].pos.x); - fix32 dy = enemies[i].pos.y - humans[j].pos.y; + 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; - bestHuman = j; + bestTreasure = j; } } - enemies[i].ints[2] = bestHuman; + enemies[i].ints[2] = bestTreasure; } - // steer toward target human + // steer toward target treasure if(enemies[i].ints[2] >= 0){ s16 t = enemies[i].ints[2]; - if(!humans[t].active || humans[t].state != HUMAN_WALKING){ + if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){ enemies[i].ints[2] = -1; } else { - fix32 dx = getWrappedDelta(humans[t].pos.x, enemies[i].pos.x); - fix32 dy = humans[t].pos.y - enemies[i].pos.y; + fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x); + fix32 dy = treasures[t].pos.y - enemies[i].pos.y; enemies[i].speed = FIX32(1.4); s16 angle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(humans[t].pos.x), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(humans[t].pos.y)); + fix32ToFix16(enemies[i].pos.x), fix32ToFix16(treasures[t].pos.x), + fix32ToFix16(enemies[i].pos.y), fix32ToFix16(treasures[t].pos.y)); enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed); enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed); @@ -382,9 +386,9 @@ void updateBuilder(u8 i){ if(adx < FIX32(16) && ady < FIX32(16)){ enemies[i].ints[3] = t; enemies[i].ints[2] = -1; - humanBeingCarried = TRUE; - humans[t].state = HUMAN_CARRIED; - humans[t].carriedBy = i; + treasureBeingCarried = TRUE; + treasures[t].state = TREASURE_CARRIED; + treasures[t].carriedBy = i; } } } @@ -449,6 +453,7 @@ static void bossPatternAimedFan(u8 i, u8 count, s16 spread, fix32 speed){ s16 aimAngle = honeAngle( fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; s16 step = (count > 1) ? (spread * 2) / (count - 1) : 0; struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, @@ -502,6 +507,7 @@ static void bossPatternWideSpray(u8 i, u8 count, fix32 speed){ s16 aimAngle = honeAngle( fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024; struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, .anim = 3 + (random() % 9), .speed = speed, .angle = aimAngle, diff --git a/src/global.h b/src/global.h index 70ca447..4ae51bb 100644 --- a/src/global.h +++ b/src/global.h @@ -8,6 +8,8 @@ void sfxPickup(); void loadMap(); void loadGame(); +#define SKIP_START 1 + u32 clock; #define CLOCK_LIMIT 32000 #define PROP_COUNT 8 @@ -20,8 +22,8 @@ u32 clock; #define CULL_LIMIT FIX32(240) -// #define MUSIC_VOLUME 50 -#define MUSIC_VOLUME 0 +#define MUSIC_VOLUME 50 +// #define MUSIC_VOLUME 0 u32 score; #define SCORE_LENGTH 8 @@ -43,8 +45,14 @@ u8 level; s16 pendingBossHp; s16 pendingBossNum; bool waitForRelease; +s16 treasureCollectedClock; +u8 treasureCollectedType; +bool allTreasureCollected; +u8 hitMessageClock; +bool hitMessageBullet; // TRUE = blasted, FALSE = smashed bool levelClearing; u32 levelClearClock; +u8 levelWaitClock; // controls struct controls { @@ -70,7 +78,7 @@ void updateControls(u16 joy, u16 changed, u16 state){ struct playerStruct { Vect2D_f32 pos, vel; s16 shotAngle; - u8 lives, recoveringClock; + u8 lives, recoveringClock, respawnClock; fix32 camera; Sprite* image; }; @@ -121,32 +129,33 @@ struct enemy { }; struct enemy enemies[ENEMY_COUNT]; -// humans -#define HUMAN_COUNT 8 -#define HUMAN_WALKING 0 -#define HUMAN_CARRIED 1 -#define HUMAN_FALLING 2 -#define HUMAN_COLLECTED 3 +// treasure +#define TREASURE_COUNT 8 +#define TREASURE_WALKING 0 +#define TREASURE_CARRIED 1 +#define TREASURE_FALLING 2 +#define TREASURE_COLLECTED 3 -struct human { +struct treasure { bool active; u8 state; + u8 type; s16 carriedBy; s16 trailIndex; Vect2D_f32 pos, vel; Sprite* image; }; -struct human humans[HUMAN_COUNT]; -bool humanBeingCarried; +struct treasure treasures[TREASURE_COUNT]; +bool treasureBeingCarried; s16 collectedCount; -void killHuman(u8 i){ - if(humans[i].state == HUMAN_CARRIED && humans[i].carriedBy >= 0){ - enemies[humans[i].carriedBy].ints[3] = -1; - humanBeingCarried = FALSE; +void killTreasure(u8 i){ + if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){ + enemies[treasures[i].carriedBy].ints[3] = -1; + treasureBeingCarried = FALSE; } - humans[i].active = FALSE; - SPR_releaseSprite(humans[i].image); + treasures[i].active = FALSE; + SPR_releaseSprite(treasures[i].image); } void killBullet(u8 i, bool explode){ @@ -181,13 +190,13 @@ void killEnemy(u8 i){ if(enemies[i].hp > 0) return; if(enemies[i].ints[3] >= 0){ s16 h = enemies[i].ints[3]; - if(humans[h].active){ - humans[h].state = HUMAN_FALLING; - humans[h].carriedBy = -1; - humans[h].vel.x = 0; - humans[h].vel.y = FIX32(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); } - humanBeingCarried = FALSE; + treasureBeingCarried = FALSE; } enemies[i].active = FALSE; SPR_releaseSprite(enemies[i].image); diff --git a/src/humans.h b/src/humans.h deleted file mode 100644 index acb136f..0000000 --- a/src/humans.h +++ /dev/null @@ -1,136 +0,0 @@ -#define HUMAN_OFF 16 - - -void spawnHuman(u8 zone){ - s16 i = -1; - for(s16 j = 0; j < HUMAN_COUNT; j++) if(!humans[j].active) { i = j; break; } - if(i == -1) return; - - humans[i].active = TRUE; - humans[i].state = HUMAN_WALKING; - humans[i].carriedBy = -1; - - fix32 zoneStart = FIX32(zone * 512); - humans[i].pos.x = zoneStart + FIX32(random() % 512); - humans[i].pos.y = GAME_H_F - FIX32(24); - - fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) }; - humans[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3]; - humans[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1); - - humans[i].image = SPR_addSprite(&koaSprite, - getScreenX(humans[i].pos.x, player.camera) - HUMAN_OFF, fix32ToInt(humans[i].pos.y) - HUMAN_OFF, - TILE_ATTR(PAL0, 0, 0, 0)); - if(!humans[i].image){ - humans[i].active = FALSE; - return; - } - SPR_setVisibility(humans[i].image, HIDDEN); -} - -static void updateHuman(u8 i){ - switch(humans[i].state){ - case HUMAN_WALKING: - // Y bounce: bob 4px around ground level - if(humans[i].pos.y >= GAME_H_F - FIX32(20) || humans[i].pos.y <= GAME_H_F - FIX32(28)) - humans[i].vel.y *= -1; - - // X wrap - if(humans[i].pos.x >= GAME_WRAP) - humans[i].pos.x -= GAME_WRAP; - if(humans[i].pos.x < 0) - humans[i].pos.x += GAME_WRAP; - - humans[i].pos.x += humans[i].vel.x; - humans[i].pos.y += humans[i].vel.y; - break; - - case HUMAN_CARRIED: - // follow carrier enemy position - if(humans[i].carriedBy >= 0 && enemies[humans[i].carriedBy].active){ - humans[i].pos.x = enemies[humans[i].carriedBy].pos.x; - humans[i].pos.y = enemies[humans[i].carriedBy].pos.y + FIX32(16); - } else { - // carrier died (shouldn't normally reach here, killEnemy handles it) - humans[i].state = HUMAN_FALLING; - humans[i].carriedBy = -1; - humans[i].vel.x = 0; - humans[i].vel.y = FIX32(3); - humanBeingCarried = FALSE; - } - break; - - case HUMAN_FALLING: - humans[i].pos.y += humans[i].vel.y; - // land on ground - if(humans[i].pos.y >= GAME_H_F - FIX32(24)){ - humans[i].pos.y = GAME_H_F - FIX32(24); - humans[i].state = HUMAN_WALKING; - fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) }; - humans[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3]; - humans[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1); - } - break; - - case HUMAN_COLLECTED: { - fix32 targetX, targetY; - if(humans[i].trailIndex == 0){ - targetX = player.pos.x; - targetY = player.pos.y + FIX32(24); - } else { - // find the human ahead in the chain - targetX = player.pos.x; - targetY = player.pos.y + FIX32(24); - for(s16 j = 0; j < HUMAN_COUNT; j++){ - if(humans[j].active && humans[j].state == HUMAN_COLLECTED - && humans[j].trailIndex == humans[i].trailIndex - 1){ - targetX = humans[j].pos.x; - targetY = humans[j].pos.y + FIX32(8); - break; - } - } - } - fix32 deltaX = getWrappedDelta(targetX, humans[i].pos.x); - humans[i].pos.x += deltaX >> 2; - humans[i].pos.y += (targetY - humans[i].pos.y) >> 2; - // X wrap - if(humans[i].pos.x >= GAME_WRAP) - humans[i].pos.x -= GAME_WRAP; - if(humans[i].pos.x < 0) - humans[i].pos.x += GAME_WRAP; - break; - } - } - - // collect: check overlap with player (walking or falling only) - fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x); - if(humans[i].state != HUMAN_CARRIED && humans[i].state != HUMAN_COLLECTED){ - fix32 dy = humans[i].pos.y - player.pos.y; - if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){ - score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000; - sfxPickup(); - humans[i].state = HUMAN_COLLECTED; - humans[i].trailIndex = collectedCount++; - return; - } - } - - s16 sx = getScreenX(humans[i].pos.x, player.camera); - s16 sy = fix32ToInt(humans[i].pos.y); - bool visible = (humans[i].state == HUMAN_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); - SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN); - SPR_setPosition(humans[i].image, sx - HUMAN_OFF, sy - HUMAN_OFF); - // manually animate flash only when walking or falling - if(humans[i].state == HUMAN_WALKING || humans[i].state == HUMAN_FALLING) - SPR_setFrame(humans[i].image, (clock / 15) & 1); - else if(humans[i].state == HUMAN_COLLECTED) - SPR_setAnim(humans[i].image, 1); - else - SPR_setFrame(humans[i].image, 0); -} - -void updateHumans(){ - for(s16 i = 0; i < HUMAN_COUNT; i++) - if(humans[i].active) - updateHuman(i); -} diff --git a/src/main.c b/src/main.c index 03efb46..88c67e8 100644 --- a/src/main.c +++ b/src/main.c @@ -5,7 +5,7 @@ #include "background.h" #include "bullets.h" #include "enemies.h" -#include "humans.h" +#include "treasure.h" #include "player.h" #include "stage.h" #include "chrome.h" @@ -28,10 +28,12 @@ void clearLevel(){ if(bullets[i].active) killBullet(i, FALSE); for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active){ enemies[i].hp = 0; killEnemy(i); } - for(s16 i = 0; i < HUMAN_COUNT; i++) - if(humans[i].active) killHuman(i); - humanBeingCarried = FALSE; + for(s16 i = 0; i < TREASURE_COUNT; i++) + if(treasures[i].active) killTreasure(i); + treasureBeingCarried = FALSE; collectedCount = 0; + allTreasureCollected = FALSE; + treasureCollectedClock = 0; // black out everything SPR_setVisibility(player.image, HIDDEN); VDP_clearTileMapRect(BG_A, 0, 0, 128, 32); @@ -46,7 +48,7 @@ void loadGame(){ XGM2_play(stageMusic); XGM2_setFMVolume(MUSIC_VOLUME); XGM2_setPSGVolume(MUSIC_VOLUME); - player.recoveringClock = 120; + player.recoveringClock = 240; killBullets = TRUE; started = TRUE; } @@ -61,32 +63,41 @@ static void updateGame(){ } if(levelClearClock >= 120){ levelClearing = FALSE; + player.pos.y = FIX32(112); + player.camera = player.pos.x - FIX32(160); + playerVelX = 0; loadBackground(); loadChrome(); loadLevel(level + 1); - XGM2_play(stageMusic); SPR_setVisibility(player.image, VISIBLE); - player.recoveringClock = 120; + player.recoveringClock = 240; killBullets = TRUE; } return; } + if(levelWaitClock > 0){ + levelWaitClock--; + if(levelWaitClock == 0){ + levelClearing = TRUE; + levelClearClock = 0; + return; + } + } if(!paused){ updatePlayer(); updateBackground(); if(clock % 2 == 0){ updateEnemies(); - if(!gameOver && enemyCount == 0){ + if(!gameOver && enemyCount == 0 && levelWaitClock == 0){ if(level >= LEVEL_COUNT - 1){ gameOver = TRUE; XGM2_stop(); } else { - levelClearing = TRUE; - levelClearClock = 0; - XGM2_stop(); + levelWaitClock = 240; + killBullets = TRUE; } } - updateHumans(); + updateTreasures(); } else { updateBullets(); } @@ -95,7 +106,8 @@ static void updateGame(){ int main(bool hardReset){ loadInternals(); - loadStart(); + if(SKIP_START) loadGame(); + else loadStart(); while(1){ if(started) updateGame(); else updateStart(); diff --git a/src/player.h b/src/player.h index 53e9548..3ce6011 100644 --- a/src/player.h +++ b/src/player.h @@ -1,9 +1,6 @@ #define PLAYER_SPEED FIX32(6) -#define PLAYER_SPEED_FOCUS FIX32(3.5) - #define PLAYER_ACCEL PLAYER_SPEED >> 4 -#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 4 #define PLAYER_OFF 24 #define PLAYER_BOUND_Y FIX32(PLAYER_OFF) @@ -25,11 +22,7 @@ static void movePlayer(){ fix32 targetVelX = 0; if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){ - if(ctrl.b){ - playerSpeed = PLAYER_SPEED_FOCUS; - } else { - playerSpeed = PLAYER_SPEED; - } + playerSpeed = PLAYER_SPEED; if(ctrl.left || ctrl.right){ if(!ctrl.a) player.shotAngle = ctrl.left ? 512 : 0; targetVelX = ctrl.left ? -playerSpeed : playerSpeed; @@ -40,10 +33,10 @@ static void movePlayer(){ } if(playerVelX < targetVelX){ - playerVelX += ctrl.b ? PLAYER_ACCEL_FOCUS : PLAYER_ACCEL; + playerVelX += PLAYER_ACCEL; if(playerVelX > targetVelX) playerVelX = targetVelX; } else if(playerVelX > targetVelX){ - playerVelX -= ctrl.b ? PLAYER_ACCEL_FOCUS : PLAYER_ACCEL; + playerVelX -= PLAYER_ACCEL; if(playerVelX < targetVelX) playerVelX = targetVelX; } @@ -105,7 +98,7 @@ static void shootPlayer(){ .player = TRUE }; void updater(s16 i){ - if(bullets[i].clock == 5) killBullet(i, TRUE); + if(bullets[i].clock == 4) killBullet(i, TRUE); } spawnBullet(spawner, updater); sfxPlayerShot(); @@ -116,11 +109,11 @@ static void shootPlayer(){ void loadPlayer(){ player.shotAngle = 0; player.camera = 0; - player.pos.x = FIX32(128); + player.pos.x = FIX32(160); player.pos.y = FIX32(112); playerVelX = 0; player.lives = 3; - player.image = SPR_addSprite(&sakuyaSprite, + player.image = SPR_addSprite(&momoyoSprite, fix32ToInt(player.pos.x) - PLAYER_OFF, fix32ToInt(player.pos.y) - PLAYER_OFF, TILE_ATTR(PAL0, 0, 0, 0)); @@ -133,6 +126,30 @@ void updatePlayer(){ return; } if(!gameOver){ + if(player.respawnClock > 0){ + // kill momentum + playerVelX = 0; + player.vel.x = 0; + player.vel.y = 0; + // lerp camera to center on player + fix32 targetCamera = player.pos.x - FIX32(160); + fix32 cameraDelta = getWrappedDelta(targetCamera, player.camera); + player.camera += cameraDelta >> 3; + // lerp player Y to center of screen + fix32 targetY = FIX32(112); + player.pos.y += (targetY - player.pos.y) >> 3; + // keep sprite position in sync so it doesn't pop on reappear + s16 sx = getScreenX(player.pos.x, player.camera); + s16 sy = fix32ToInt(player.pos.y); + SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); + player.respawnClock--; + if(player.respawnClock == 0){ + SPR_setVisibility(player.image, VISIBLE); + player.recoveringClock = 240; + killBullets = TRUE; + } + return; + } if(player.recoveringClock > 0){ if(player.recoveringClock % 10 == 1) SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN); diff --git a/src/stage.h b/src/stage.h index 7ca21c9..3b63da7 100644 --- a/src/stage.h +++ b/src/stage.h @@ -3,7 +3,7 @@ // ============================================================================= // // Each level is a single struct defining what spawns. The level ends when all -// enemies are killed (enemyCount == 0). Humans are bonus -- they don't affect +// enemies are killed (enemyCount == 0). Treasures are bonus -- they don't affect // level completion. // // --- STRUCT FIELDS --- @@ -24,12 +24,12 @@ // Very dangerous -- 2-3 is oppressive, 6 is near-impossible. // Good range: 0-6. Introduce after players learn movement. // -// builders Number of Builder enemies (type 4). Human abductor. -// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking humans -// and flies upward. If it reaches the top, the human dies and a -// Gunner spawns in its place. Only 1 human can be carried at a time. +// builders Number of Builder enemies (type 4). Treasure abductor. +// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking treasures +// and flies upward. If it reaches the top, the treasure dies and a +// Gunner spawns in its place. Only 1 treasure can be carried at a time. // Creates urgency -- player must choose between killing enemies -// and saving humans. Good range: 0-2. +// and saving treasures. Good range: 0-2. // // bossHp If > 0, spawns a Boss enemy (type 5) with this many HP. // Boss number is auto-calculated from level index (lvl / 4). @@ -38,11 +38,11 @@ // Typical values: 25, 50, 75, 100, 125. // Other enemies can coexist with the boss (adds pressure). // -// humans Number of humans (koakuma) to spawn. Distributed across 4 zones -// (2 per zone if >= 4 humans, then 1 each for remainder). +// treasures Number of treasures to spawn. Distributed across 4 zones +// (2 per zone if >= 4 treasures, then 1 each for remainder). // Walk along the ground, can be collected by player for 1000 pts // (2000 if caught mid-fall after enemy drops them). -// Max 8 (HUMAN_COUNT). Usually just set to 8. +// Max 8 (TREASURE_COUNT). Usually just set to 8. // // gunnerPattern Controls what bullet pattern gunners use: // 0 = Radial Burst: 8 bullets in a circle every 60 frames. @@ -63,7 +63,7 @@ // Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss // must not exceed 24. If it does, excess enemies silently fail to spawn. // -// Total humans: 8 slots (HUMAN_COUNT). +// Total treasures: 8 slots (TREASURE_COUNT). // // Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets // get priority and evict enemy bullets when full. @@ -81,7 +81,7 @@ // - Drone-heavy levels (12-16) create constant movement pressure // - Gunner-heavy levels (4-6) create bullet reading / dodging challenges // - Hunter levels force the player to keep moving (anti-camping) -// - Builder levels force tough choices: kill builders or save humans? +// - Builder levels force tough choices: kill builders or save treasures? // - Combining hunters + gunners is very hard (dodge bullets while fleeing) // - Boss levels with escort enemies (drones/gunners alongside boss) are // harder than solo boss fights @@ -93,12 +93,12 @@ struct LevelDef { u8 drones, gunners, hunters, builders; u8 bossHp; - u8 humans; + u8 treasures; u8 gunnerPattern; bool dronesShoot; }; -// dr gn hn bl boss hum pat shoot +// dr gn hn bl boss tre pat shoot const struct LevelDef levels[20] = { // Phase 1: "Immediate danger" (L1-L4) { 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1 @@ -168,13 +168,13 @@ void loadLevel(u8 lvl){ spawnEnemy(ENEMY_TYPE_BOSS, 1); } - // spawn humans - u8 humansToSpawn = def->humans; - for(u8 zone = 0; zone < 4 && humansToSpawn > 0; zone++){ - u8 perZone = humansToSpawn >= 4 ? 2 : 1; - for(u8 h = 0; h < perZone && humansToSpawn > 0; h++){ - spawnHuman(zone); - humansToSpawn--; + // spawn treasures + u8 treasureToSpawn = def->treasures; + for(u8 zone = 0; zone < 4 && 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/start.h b/src/start.h index 422fcf2..13ce875 100644 --- a/src/start.h +++ b/src/start.h @@ -1,15 +1,82 @@ -#define START_I 8 +#define START_W 40 +#define START_H 28 +#define START_I 1 +#define TRANS_TIME 200 + +s16 startClock; + +static void updateTransition(s16 startTime, bool last){ + if(startClock >= startTime && startClock < startTime + TRANS_TIME){ + switch(startClock - startTime){ + case 0: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break; + case 5: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break; + case 10: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break; + case 15: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H); break; + case 20: VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H); break; + case TRANS_TIME - 20: if(!last){ + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H); + } break; + case TRANS_TIME - 15: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break; + case TRANS_TIME - 10: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break; + case TRANS_TIME - 5: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break; + } + } +} + +static void drawStartSplash(){ + VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I), 0, 0, START_W, START_H); + VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 13, 7, 0, DMA); +} + +static void drawStartBg(){ + VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H); + VDP_drawImageEx(BG_B, &startBigBg, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16), 0, 0, 0, DMA); +} + +static void drawStartMenu(){ + // VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 16 + 256), 26, 2, 0, DMA); + VDP_drawText(" PRESS ANY", 19, 18); + VDP_drawText(" BUTTON", 19, 19); + VDP_drawText(" T. BODDY", 19, 24); + VDP_drawText(" 02.2026", 19, 25); +} + +static void loadGameFromStart(){ + XGM2_stop(); + setRandomSeed(startClock); + VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H); + VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H); + loadGame(); +} + +s16 startTime; +static void updateStartMenu(){ + if(startTime == 0 && (ctrl.start || ctrl.a || ctrl.b || ctrl.c)){ + XGM2_stop(); + startTime = 30; + } + if(startTime > 0){ + startTime--; + if(startTime <= 0){ + loadGameFromStart(); + } + } +} void loadStart(){ - VDP_drawImageEx(BG_A, &logo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I), 6, 10, FALSE, FALSE); - VDP_drawText("press any button", 12, 16); - VDP_drawText(" 2026 T.BODDY ", 12, 18); + VDP_loadTileSet(startFade1.tileset, START_I, DMA); + VDP_loadTileSet(startFade2.tileset, START_I + 1, DMA); + VDP_loadTileSet(startFade3.tileset, START_I + 2, DMA); + VDP_loadTileSet(startFade4.tileset, START_I + 3, DMA); + drawStartSplash(); + XGM2_play(bgmStart); } void updateStart(){ - if(ctrl.a || ctrl.b || ctrl.c || ctrl.start){ - VDP_clearTileMapRect(BG_A, 0, 0, 40, 28); - waitForRelease = TRUE; - loadGame(); - } -} \ No newline at end of file + updateTransition(0, FALSE); + updateTransition(TRANS_TIME, TRUE); + if(startClock == TRANS_TIME) drawStartBg(); + else if(startClock == TRANS_TIME + 40) drawStartMenu(); + else if(startClock > TRANS_TIME + 40) updateStartMenu(); + if(startClock < CLOCK_LIMIT) startClock++; +} diff --git a/src/treasure.h b/src/treasure.h new file mode 100644 index 0000000..f737a3b --- /dev/null +++ b/src/treasure.h @@ -0,0 +1,159 @@ +#define TREASURE_OFF 16 + + +void spawnTreasure(u8 zone){ + s16 i = -1; + for(s16 j = 0; j < TREASURE_COUNT; j++) if(!treasures[j].active) { i = j; break; } + if(i == -1) return; + + treasures[i].active = TRUE; + treasures[i].state = TREASURE_WALKING; + treasures[i].carriedBy = -1; + + fix32 zoneStart = FIX32(zone * 512); + treasures[i].pos.x = zoneStart + FIX32(random() % 512); + treasures[i].pos.y = GAME_H_F - FIX32(24); + + fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) }; + treasures[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3]; + treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1); + + treasures[i].image = SPR_addSprite(&treasureSprite, + getScreenX(treasures[i].pos.x, player.camera) - TREASURE_OFF, fix32ToInt(treasures[i].pos.y) - TREASURE_OFF, + TILE_ATTR(PAL0, 0, 0, 0)); + if(!treasures[i].image){ + treasures[i].active = FALSE; + return; + } + SPR_setVisibility(treasures[i].image, HIDDEN); + treasures[i].type = random() % 4; + SPR_setAnim(treasures[i].image, treasures[i].type); +} + +static void updateTreasure(u8 i){ + switch(treasures[i].state){ + case TREASURE_WALKING: + // Y bounce: bob 4px around ground level + if(treasures[i].pos.y >= GAME_H_F - FIX32(20) || treasures[i].pos.y <= GAME_H_F - FIX32(28)) + treasures[i].vel.y *= -1; + + // X wrap + if(treasures[i].pos.x >= GAME_WRAP) + treasures[i].pos.x -= GAME_WRAP; + if(treasures[i].pos.x < 0) + treasures[i].pos.x += GAME_WRAP; + + treasures[i].pos.x += treasures[i].vel.x; + treasures[i].pos.y += treasures[i].vel.y; + break; + + case TREASURE_CARRIED: + // follow carrier enemy position + if(treasures[i].carriedBy >= 0 && enemies[treasures[i].carriedBy].active){ + treasures[i].pos.x = enemies[treasures[i].carriedBy].pos.x; + treasures[i].pos.y = enemies[treasures[i].carriedBy].pos.y + FIX32(16); + } else { + // carrier died (shouldn't normally reach here, killEnemy handles it) + treasures[i].state = TREASURE_FALLING; + treasures[i].carriedBy = -1; + treasures[i].vel.x = 0; + treasures[i].vel.y = FIX32(3); + treasureBeingCarried = FALSE; + } + break; + + case TREASURE_FALLING: + treasures[i].pos.y += treasures[i].vel.y; + // land on ground + if(treasures[i].pos.y >= GAME_H_F - FIX32(24)){ + treasures[i].pos.y = GAME_H_F - FIX32(24); + treasures[i].state = TREASURE_WALKING; + fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) }; + treasures[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3]; + treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1); + } + break; + + case TREASURE_COLLECTED: { + fix32 targetX, targetY; + if(treasures[i].trailIndex == 0){ + targetX = player.pos.x; + targetY = player.pos.y + FIX32(24); + } else { + // find the treasure ahead in the chain + targetX = player.pos.x; + targetY = player.pos.y + FIX32(24); + for(s16 j = 0; j < TREASURE_COUNT; j++){ + if(treasures[j].active && treasures[j].state == TREASURE_COLLECTED + && treasures[j].trailIndex == treasures[i].trailIndex - 1){ + targetX = treasures[j].pos.x; + targetY = treasures[j].pos.y + FIX32(8); + break; + } + } + } + fix32 deltaX = getWrappedDelta(targetX, treasures[i].pos.x); + treasures[i].pos.x += deltaX >> 2; + treasures[i].pos.y += (targetY - treasures[i].pos.y) >> 2; + // X wrap + if(treasures[i].pos.x >= GAME_WRAP) + treasures[i].pos.x -= GAME_WRAP; + if(treasures[i].pos.x < 0) + treasures[i].pos.x += GAME_WRAP; + break; + } + } + + // collect: check overlap with player (walking or falling only) + fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x); + if(treasures[i].state != TREASURE_CARRIED && treasures[i].state != TREASURE_COLLECTED){ + fix32 dy = treasures[i].pos.y - player.pos.y; + if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){ + score += (treasures[i].state == TREASURE_FALLING) ? 2000 : 1000; + sfxPickup(); + treasureCollectedType = treasures[i].type; + treasureCollectedClock = 120; + // only add to trail if this type isn't already collected + bool duplicate = FALSE; + for(s16 j = 0; j < TREASURE_COUNT; j++){ + if(treasures[j].active && treasures[j].state == TREASURE_COLLECTED + && treasures[j].type == treasures[i].type){ + duplicate = TRUE; + break; + } + } + if(duplicate){ + killTreasure(i); + } else { + treasures[i].state = TREASURE_COLLECTED; + treasures[i].trailIndex = collectedCount++; + } + return; + } + } + + s16 sx = getScreenX(treasures[i].pos.x, player.camera); + s16 sy = fix32ToInt(treasures[i].pos.y); + bool visible = (treasures[i].state == TREASURE_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); + if(visible && treasures[i].state == TREASURE_COLLECTED){ + if(player.respawnClock > 0) + visible = FALSE; + else if(player.recoveringClock > 0) + visible = (player.recoveringClock % 20 > 10); + } + SPR_setVisibility(treasures[i].image, visible ? VISIBLE : HIDDEN); + SPR_setPosition(treasures[i].image, sx - TREASURE_OFF, sy - TREASURE_OFF); + // frame 0 = normal, frame 1 = flash, frame 2 = collected/faded + if(treasures[i].state == TREASURE_WALKING || treasures[i].state == TREASURE_FALLING) + SPR_setFrame(treasures[i].image, (clock / 15) & 1); + else if(treasures[i].state == TREASURE_COLLECTED) + SPR_setFrame(treasures[i].image, 2); + else + SPR_setFrame(treasures[i].image, 0); +} + +void updateTreasures(){ + for(s16 i = 0; i < TREASURE_COUNT; i++) + if(treasures[i].active) + updateTreasure(i); +}