shit lmao

This commit is contained in:
t. boddy 2026-02-18 22:05:46 -05:00
parent 06e8e735fb
commit 6adbe1882d
40 changed files with 608 additions and 361 deletions

BIN
res/boss.vgm Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

BIN
res/level.vgm Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

BIN
res/momoyo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

@ -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"
XGM2 stageMusic "level.vgm"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

BIN
res/skyred.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

BIN
res/start.vgm Normal file

Binary file not shown.

BIN
res/start/bigbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
res/start/fade1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
res/start/fade2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
res/start/fade3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
res/start/fade4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
res/start/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
res/start/splash1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
res/treasure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
res/treasure.vgm Normal file

Binary file not shown.

View file

@ -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;
// }
}

View file

@ -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;
}
}
}

View file

@ -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();
}

View file

@ -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;
}
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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);
}

View file

@ -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();

View file

@ -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;
}
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);

View file

@ -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--;
}
}

View file

@ -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();
}
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++;
}

159
src/treasure.h Normal file
View file

@ -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);
}