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

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