This commit is contained in:
t. boddy 2026-03-18 13:23:51 -04:00
parent 417cae168f
commit a8bc01bedd
59 changed files with 2053 additions and 1054 deletions

View file

@ -1,11 +1,13 @@
#define BG_I 8
// zone-unique block: 64x64px ground block in sky area, only visible in zone 0
// world X=256 = tile col 32, placed in sky row block 1 (tile row 8)
#define ZONE_BLOCK_WORLD_X 256
#define ZONE_BLOCK_COL ((ZONE_BLOCK_WORLD_X / 8) % 128)
// doors: one per zone, placed in sky area at tile row 16
// base X per zone chosen so tile cols never overlap between zone pairs:
// zone 0 → cols 1-31, zone 2 → cols 33-63
// zone 1 → cols 65-95, zone 3 → cols 97-127
#define ZONE_BLOCK_ROW 16
bool zoneBlockVisible;
#define DOOR_COUNT SECTION_COUNT
fix32 doorWorldX[DOOR_COUNT];
bool doorVisible[DOOR_COUNT];
fix32 prevCamera;
#define PARALLAX_COUNT 8
fix32 parallaxAccum[PARALLAX_COUNT];
@ -15,6 +17,7 @@ static const fix32 parallaxMul[PARALLAX_COUNT] = {
s16 bgScroll[28];
u8 bgOff;
u16 bgPal[16];
void loadBackground(){
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
@ -23,6 +26,7 @@ void loadBackground(){
VDP_loadTileSet(sky.tileset, BG_I + 64, DMA);
VDP_loadTileSet(ground.tileset, BG_I + 128, DMA);
VDP_loadTileSet(skyRed.tileset, BG_I + 192, DMA);
VDP_loadTileSet(door.tileset, BG_I + 256, DMA);
// for(u8 y = 0; y < 14; y++){
// for(u8 x = 0; x < 64; x++){
@ -41,29 +45,35 @@ void loadBackground(){
// }
// }
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 192), 0, 0, 128, 8);
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 192), 0, 0, 128, 8);
for(u8 y = 0; y < 3; y++){
for(u8 x = 0; x < 16; x++){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 * y), x * 8, y * 8 + 8, 8, 8);
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 64 * y), x * 8, y * 8 + 8, 8, 8);
}
}
// place 64x64 ground block in sky area (zone 0 only)
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
zoneBlockVisible = TRUE;
// place one door per zone at a random position within each zone's unique col range
for(u8 d = 0; d < DOOR_COUNT; d++){
doorWorldX[d] = FIX32(d * 512 + 8 + (random() % 31) * 8);
u8 col = (u8)(F32_toInt(doorWorldX[d]) / 8 % 128);
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 256), col, ZONE_BLOCK_ROW, 8, 8);
doorVisible[d] = TRUE;
}
prevCamera = player.camera;
for(u8 i = 0; i < PARALLAX_COUNT; i++)
parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]);
parallaxAccum[i] = F32_mul(player.camera + FIX32(256), parallaxMul[i]);
// write initial scroll values so first frame has correct parallax
s16 initScroll = fix32ToInt(-player.camera);
s16 initScroll = F32_toInt(-player.camera);
for(u8 i = 0; i < 20; i++)
bgScroll[i] = initScroll;
for(u8 i = 0; i < 8; i++)
bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i]));
bgScroll[27 - i] = (initScroll - F32_toInt(parallaxAccum[i]));
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
}
void updateBackground(){
s16 scrollVal = fix32ToInt(-player.camera);
s16 scrollVal = F32_toInt(-player.camera);
// accumulate parallax from camera delta (not absolute position)
// this avoids discontinuities at world wrap boundaries
@ -75,7 +85,7 @@ void updateBackground(){
// update accumulators once, reuse for top and bottom
for(u8 i = 0; i < PARALLAX_COUNT; i++){
parallaxAccum[i] += fix32Mul(delta, parallaxMul[i]);
parallaxAccum[i] += F32_mul(delta, parallaxMul[i]);
if(parallaxAccum[i] > FIX32(1024)) parallaxAccum[i] -= FIX32(1024);
else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024);
}
@ -83,18 +93,51 @@ void updateBackground(){
for(u8 i = 0; i < 20; i++)
bgScroll[i] = scrollVal;
for(u8 i = 0; i < 8; i++)
bgScroll[27 - i] = (scrollVal - fix32ToInt(parallaxAccum[i]));
bgScroll[27 - i] = (scrollVal - F32_toInt(parallaxAccum[i]));
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
// show ground block only when zone 0 copy of these columns is on screen
fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160));
bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212));
// if(shouldShow && !zoneBlockVisible){
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
// zoneBlockVisible = TRUE;
// } else if(!shouldShow && zoneBlockVisible){
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
// zoneBlockVisible = FALSE;
// }
// show/hide each door based on proximity to camera center
for(u8 d = 0; d < DOOR_COUNT; d++){
fix32 dx = getWrappedDelta(doorWorldX[d] + FIX32(32), player.camera + FIX32(160));
bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212));
u8 col = (u8)(F32_toInt(doorWorldX[d]) / 8 % 128);
if(shouldShow && !doorVisible[d]){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 256), col, ZONE_BLOCK_ROW, 8, 8);
doorVisible[d] = TRUE;
} else if(!shouldShow && doorVisible[d]){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, BG_I + 64), col, ZONE_BLOCK_ROW, 8, 8);
doorVisible[d] = FALSE;
}
}
}
#define BG_THEME_RED 0
#define BG_THEME_GREEN 1
#define BG_THEME_BLUE 2
void loadBgPalette(u8 theme) {
u16 coloredPalette[16];
u8 i;
for(i = 0; i < 16; i++) {
u16 color = shadow.palette->data[i];
u16 r = color & 0xF;
u16 g = (color >> 4) & 0xF;
u16 b = (color >> 8) & 0xF;
switch(theme) {
case BG_THEME_GREEN:
coloredPalette[i] = (b << 8) | (r << 4) | g;
break;
case BG_THEME_BLUE: {
u16 newB = r > b ? r : b;
coloredPalette[i] = (newB << 8) | (g << 4) | (r >> 1);
break;
}
default: // BG_THEME_RED
coloredPalette[i] = color;
break;
}
}
memcpy(bgPal, coloredPalette, 16 * sizeof(u16));
PAL_setPalette(PAL2, coloredPalette, DMA_QUEUE);
}

View file

@ -92,126 +92,6 @@ SkipInit:
*
*------------------------------------------------
registersDump:
move.l %d0,registerState+0
move.l %d1,registerState+4
move.l %d2,registerState+8
move.l %d3,registerState+12
move.l %d4,registerState+16
move.l %d5,registerState+20
move.l %d6,registerState+24
move.l %d7,registerState+28
move.l %a0,registerState+32
move.l %a1,registerState+36
move.l %a2,registerState+40
move.l %a3,registerState+44
move.l %a4,registerState+48
move.l %a5,registerState+52
move.l %a6,registerState+56
move.l %a7,registerState+60
rts
busAddressErrorDump:
move.w 4(%sp),ext1State
move.l 6(%sp),addrState
move.w 10(%sp),ext2State
move.w 12(%sp),srState
move.l 14(%sp),pcState
jmp registersDump
exception4WDump:
move.w 4(%sp),srState
move.l 6(%sp),pcState
move.w 10(%sp),ext1State
jmp registersDump
exceptionDump:
move.w 4(%sp),srState
move.l 6(%sp),pcState
jmp registersDump
_Bus_Error:
jsr busAddressErrorDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l busErrorCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Address_Error:
jsr busAddressErrorDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l addressErrorCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Illegal_Instruction:
jsr exception4WDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l illegalInstCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Zero_Divide:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l zeroDivideCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Chk_Instruction:
jsr exception4WDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l chkInstCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Trapv_Instruction:
jsr exception4WDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l trapvInstCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Privilege_Violation:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l privilegeViolationCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Trace:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l traceCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Line_1010_Emulation:
_Line_1111_Emulation:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l line1x1xCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_Error_Exception:
jsr exceptionDump
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l errorExceptionCB, %a0
jsr (%a0)
movem.l (%sp)+,%d0-%d1/%a0-%a1
rte
_INT:
movem.l %d0-%d1/%a0-%a1,-(%sp)
move.l intCB, %a0

View file

@ -1,68 +1,39 @@
#define BULLET_OFF 8
#define P_BULLET_OFF 16
static void doBulletRotation(u8 i){
if(bullets[i].anim >= FIRST_ROTATING_BULLET && !bullets[i].player){
void doBulletRotation(u8 i){
if(bullets[i].anim >= FIRST_ROTATING_BULLET){
bullets[i].vFlip = FALSE;
bullets[i].hFlip = FALSE;
if(bullets[i].angle < 0) bullets[i].angle += 1024;
else if(bullets[i].angle >= 1024) bullets[i].angle -= 1024;
fix16 a = F16_normalizeAngle(bullets[i].angle);
s16 deg = F16_toInt(a);
u8 quadrant = deg / 90;
u8 inQuad = deg % 90;
u8 frame = (inQuad * 9) / 90;
switch(quadrant){
case 0: break;
case 1: frame = 8 - frame; bullets[i].hFlip = TRUE; break;
case 2: bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; break;
case 3: frame = 8 - frame; bullets[i].vFlip = TRUE; break;
}
// 0 - 256
if(bullets[i].angle >= 1008 || bullets[i].angle < 16) bullets[i].frame = 0;
else if(bullets[i].angle >= 16 && bullets[i].angle < 48) bullets[i].frame = 1;
else if(bullets[i].angle >= 48 && bullets[i].angle < 80) bullets[i].frame = 2;
else if(bullets[i].angle >= 80 && bullets[i].angle < 112) bullets[i].frame = 3;
else if(bullets[i].angle >= 112 && bullets[i].angle < 144) bullets[i].frame = 4;
else if(bullets[i].angle >= 112 && bullets[i].angle < 176) bullets[i].frame = 5;
else if(bullets[i].angle >= 176 && bullets[i].angle < 208) bullets[i].frame = 6;
else if(bullets[i].angle >= 208 && bullets[i].angle < 240) bullets[i].frame = 7;
else if(bullets[i].angle >= 240 && bullets[i].angle < 272) bullets[i].frame = 8;
// 256 - 512
else if(bullets[i].angle >= 272 && bullets[i].angle < 304) { bullets[i].frame = 7; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 304 && bullets[i].angle < 336) { bullets[i].frame = 6; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 336 && bullets[i].angle < 368) { bullets[i].frame = 5; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 368 && bullets[i].angle < 400) { bullets[i].frame = 4; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 400 && bullets[i].angle < 432) { bullets[i].frame = 3; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 432 && bullets[i].angle < 464) { bullets[i].frame = 2; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 464 && bullets[i].angle < 496) { bullets[i].frame = 1; bullets[i].hFlip = TRUE; }
else if(bullets[i].angle >= 496 && bullets[i].angle < 528) { bullets[i].frame = 0; bullets[i].hFlip = TRUE; }
// 512 - 768
else if(bullets[i].angle >= 528 && bullets[i].angle < 560) { bullets[i].frame = 1; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 560 && bullets[i].angle < 592) { bullets[i].frame = 2; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 592 && bullets[i].angle < 624) { bullets[i].frame = 3; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 624 && bullets[i].angle < 656) { bullets[i].frame = 4; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 656 && bullets[i].angle < 688) { bullets[i].frame = 5; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 688 && bullets[i].angle < 720) { bullets[i].frame = 6; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 720 && bullets[i].angle < 752) { bullets[i].frame = 7; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 752 && bullets[i].angle < 784) { bullets[i].frame = 8; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; }
// 768 - 1024
else if(bullets[i].angle >= 784 && bullets[i].angle < 816) { bullets[i].frame = 7; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 816 && bullets[i].angle < 848) { bullets[i].frame = 6; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 848 && bullets[i].angle < 880) { bullets[i].frame = 5; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 880 && bullets[i].angle < 912) { bullets[i].frame = 4; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 912 && bullets[i].angle < 944) { bullets[i].frame = 3; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 944 && bullets[i].angle < 976) { bullets[i].frame = 2; bullets[i].vFlip = TRUE; }
else if(bullets[i].angle >= 976 && bullets[i].angle < 1008) { bullets[i].frame = 1; bullets[i].vFlip = TRUE; }
bullets[i].frame = frame;
SPR_setFrame(bullets[i].image, bullets[i].frame);
SPR_setHFlip(bullets[i].image, bullets[i].hFlip);
SPR_setVFlip(bullets[i].image, bullets[i].vFlip);
}
}
void spawnBullet(struct bulletSpawner spawner, void(*updater)){
if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return;
// Don't spawn if offscreen
bool spawnBullet(struct bulletSpawner spawner, void(*updater)){
if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return FALSE;
// Don't spawn if offscreen -- enemy bullets use tighter visible-screen limit
fix32 dx = getWrappedDelta(spawner.x, player.pos.x);
bool offScreenX = (dx < -CULL_LIMIT || dx > CULL_LIMIT);
fix32 cullX = spawner.player ? CULL_LIMIT : SCREEN_LIMIT;
bool offScreenX = (dx < -cullX || dx > cullX);
bool offScreenY = (spawner.y < FIX32(-16) || spawner.y > GAME_H_F + FIX32(16));
if(offScreenX || offScreenY) return;
if(offScreenX || offScreenY) return FALSE;
// Find available slot, return if none
s16 i = -1;
@ -74,32 +45,37 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){
break;
}
}
if(i == -1) return;
if(i == -1) return FALSE;
spawner.angle = F16_normalizeAngle(spawner.angle);
bullets[i].active = TRUE;
bullets[i].pos.x = spawner.x;
bullets[i].pos.y = spawner.y;
bullets[i].angle = spawner.angle;
bullets[i].speed = spawner.speed;
bullets[i].player = spawner.player;
bullets[i].clock = 0;
if(spawner.vel.x || spawner.vel.y){
bullets[i].vel.x = spawner.vel.x;
bullets[i].vel.y = spawner.vel.y;
} else {
bullets[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(spawner.angle)), spawner.speed);
bullets[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(spawner.angle)), spawner.speed);
bullets[i].vel.x = F32_mul(F32_cos(spawner.angle), spawner.speed);
bullets[i].vel.y = F32_mul(F32_sin(spawner.angle), spawner.speed);
}
bullets[i].updater = updater;
bullets[i].explosion = FALSE;
bullets[i].dist = bullets[i].player ? 16 : (spawner.anim == 0 ? 4 : 7);
bullets[i].grazed = FALSE;
bullets[i].dist = bullets[i].player ? 24 : (spawner.anim == 0 ? 4 : 7);
// zero out ints array
for(s16 j = 0; j < PROP_COUNT; j++) bullets[i].ints[j] = spawner.ints[j];
bullets[i].image = SPR_addSprite(spawner.player ? &pBulletSprite : &bulletsSprite,
bullets[i].image = SPR_addSprite(&bulletsSprite,
-32, -32,
TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, spawner.player && spawner.angle == 512 ? 1 : 0));
TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, 0));
if(!bullets[i].image){
bullets[i].active = FALSE;
return;
return FALSE;
}
bullets[i].anim = spawner.anim;
@ -108,6 +84,12 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){
SPR_setFrame(bullets[i].image, spawner.frame);
SPR_setDepth(bullets[i].image, spawner.player ? 7 : (spawner.top ? 3 : 4));
doBulletRotation(i);
return TRUE;
}
void updateBulletVel(u8 i){
bullets[i].vel.x = F32_mul(F32_cos(bullets[i].angle), bullets[i].speed);
bullets[i].vel.y = F32_mul(F32_sin(bullets[i].angle), bullets[i].speed);
}
#define BULLET_CHECK FIX32(32)
@ -119,9 +101,9 @@ static void collideWithEnemy(u8 i){
fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y;
if(deltaY >= -BULLET_CHECK && deltaY <= BULLET_CHECK &&
deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){
bulletDist = getApproximatedDistance(fix32ToInt(deltaX), fix32ToInt(deltaY));
bulletDist = getApproximatedDistance(F32_toInt(deltaX), F32_toInt(deltaY));
if(bulletDist <= bullets[i].dist){
score += (enemies[j].ints[3] >= 0) ? 200 : 100;
score += (enemies[j].ints[3] >= 0) ? 512 : 256;
killBullet(i, TRUE);
killEnemy(j);
sfxExplosion();
@ -137,27 +119,54 @@ static void collideWithPlayer(u8 i){
fix32 deltaY = bullets[i].pos.y - player.pos.y;
s32 dist = getApproximatedDistance(
fix32ToInt(deltaX),
fix32ToInt(deltaY));
F32_toInt(deltaX),
F32_toInt(deltaY));
if(dist <= 4){
// convert enemy bullet to player bullet explosion in-place
SPR_setDefinition(bullets[i].image, &pBulletSprite);
bullets[i].player = TRUE;
bullets[i].pos.x = player.pos.x;
bullets[i].pos.y = player.pos.y;
killBullet(i, TRUE);
sfxExplosion();
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
} else {
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = TRUE;
// kill enemy bullet, then spawn a fresh player bullet explosion
killBullet(i, FALSE);
s16 expSlot = -1;
for(s16 j = 0; j < BULLET_COUNT; j++) if(!bullets[j].active){ expSlot = j; break; }
if(expSlot >= 0){
bullets[expSlot].active = TRUE;
bullets[expSlot].player = TRUE;
bullets[expSlot].explosion = TRUE;
bullets[expSlot].pos.x = player.pos.x;
bullets[expSlot].pos.y = player.pos.y;
bullets[expSlot].vel.x = 0;
bullets[expSlot].vel.y = 0;
bullets[expSlot].clock = 0;
bullets[expSlot].frame = 0;
bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0));
if(bullets[expSlot].image){
SPR_setDepth(bullets[expSlot].image, 5);
SPR_setAnim(bullets[expSlot].image, 1);
SPR_setFrame(bullets[expSlot].image, 0);
SPR_setHFlip(bullets[expSlot].image, random() & 1);
} else {
bullets[expSlot].active = FALSE;
}
}
sfxExplosion();
if(!isAttract){
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
sfxGameOver();
} else {
sfxPlayerHit();
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = TRUE;
}
}
} else if(dist <= GRAZE_RADIUS && !bullets[i].grazed){
bullets[i].grazed = TRUE;
score += 64;
grazeCount++;
sfxGraze();
}
}
@ -172,8 +181,8 @@ static void updateBulletExplosion(u8 i){
SPR_setFrame(bullets[i].image, bullets[i].frame);
}
s16 sx = getScreenX(bullets[i].pos.x, player.camera);
s16 sy = fix32ToInt(bullets[i].pos.y);
u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF;
s16 sy = F32_toInt(bullets[i].pos.y);
u8 off = BULLET_OFF;
SPR_setPosition(bullets[i].image, sx - off, sy - off);
}
@ -182,8 +191,8 @@ static void updateBullet(u8 i){
updateBulletExplosion(i);
return;
}
bullets[i].pos.x += bullets[i].vel.x;
bullets[i].pos.y += bullets[i].vel.y;
bullets[i].pos.x += bullets[i].vel.x - (player.vel.x >> 3);
bullets[i].pos.y += bullets[i].vel.y - (playerScrollVelY >> 3);
if(bullets[i].pos.x >= GAME_WRAP){
bullets[i].pos.x -= GAME_WRAP;
@ -203,7 +212,7 @@ static void updateBullet(u8 i){
return;
}
if(offScreenBottom){
killBullet(i, TRUE);
killBullet(i, FALSE);
return;
}
if(bullets[i].clock > 0) bullets[i].updater(i);
@ -211,8 +220,8 @@ static void updateBullet(u8 i){
else if(!gameOver) collideWithPlayer(i);
if(bullets[i].active){
s16 sx = getScreenX(bullets[i].pos.x, player.camera);
s16 sy = fix32ToInt(bullets[i].pos.y);
u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF;
s16 sy = F32_toInt(bullets[i].pos.y);
u8 off = BULLET_OFF;
SPR_setPosition(bullets[i].image, sx - off, sy - off);
bullets[i].clock++;
bulletCount++;
@ -225,7 +234,7 @@ void updateBullets(){
if(killBullets){
killBullets = FALSE;
for(s16 i = 0; i < BULLET_COUNT; i++)
if(bullets[i].active && !bullets[i].player) killBullet(i, TRUE);
if(bullets[i].active && !bullets[i].player && !bullets[i].explosion) killBullet(i, TRUE);
}
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active)
updateBullet(i);

View file

@ -1,17 +1,20 @@
#define MAP_I 512
#define MAP_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I)
#define MAP_PLAYER_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 1)
#define MAP_ENEMY_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 2)
#define MAP_TREASURE_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3)
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4)
#define MAP_I 328
#define MAP_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I)
#define MAP_PLAYER_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 1)
#define MAP_ENEMY_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 2)
#define MAP_BOSS_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 9)
#define MAP_TREASURE_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 3)
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 4)
#define FONT_BIG_I 256
u16 hudPal = PAL0;
#define FONT_BIG_I 340
void bigText(char* str, u16 x, u16 y, bool shadow){
for(u8 i = 0; i < strlen(str); i++){
if(str[i] >= 48){
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + str[i] - 48), x + i, y);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + 16 + str[i] - 48), x + i, y + 1);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + str[i] - 48), x + i, y);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + 16 + str[i] - 48), x + i, y + 1);
}
}
}
@ -31,7 +34,7 @@ s16 lastLives;
static void drawLives(){
VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16);
for(u8 i = 0; i < (player.lives - 1); i++)
VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, LIFE_I + (i > 0 ? 2 : 0)), LIVES_X, LIVES_Y + i, 1, 2);
VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, LIFE_I + (i > 0 ? 2 : 0)), LIVES_X, LIVES_Y + i, 1, 2);
lastLives = player.lives;
}
@ -57,17 +60,17 @@ static void drawScore(){
void loadMap(){
VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1);
for(s16 i = 0; i < ENEMY_COUNT; i++){
mapEnemyCol[i] = -1;
@ -96,7 +99,7 @@ static bool mapTileOccupied(s16 col, s16 row, s16 pRow){
static void updateMap(){
// compute new player row
s16 pRow = fix32ToInt(player.pos.y) / 75;
s16 pRow = F32_toInt(player.pos.y) / 75;
if(pRow < 0) pRow = 0;
if(pRow >= MAP_H) pRow = MAP_H - 1;
@ -109,10 +112,10 @@ static void updateMap(){
continue;
}
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
s16 col = fix32ToInt(dx) / 54 + MAP_W / 2;
s16 col = F32_toInt(dx) / MAP_SCALE + MAP_W / 2;
if(col < 0) col = 0;
if(col >= MAP_W) col = MAP_W - 1;
s16 row = fix32ToInt(enemies[i].pos.y) / 75;
s16 row = F32_toInt(enemies[i].pos.y) / 75;
if(row < 0) row = 0;
if(row >= MAP_H) row = MAP_H - 1;
mapNewCol[i] = col;
@ -127,10 +130,10 @@ static void updateMap(){
continue;
}
fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x);
s16 col = fix32ToInt(dx) / 54 + MAP_W / 2;
s16 col = F32_toInt(dx) / MAP_SCALE + MAP_W / 2;
if(col < 0) col = 0;
if(col >= MAP_W) col = MAP_W - 1;
s16 row = fix32ToInt(treasures[i].pos.y) / 75;
s16 row = F32_toInt(treasures[i].pos.y) / 75;
if(row < 0) row = 0;
if(row >= MAP_H) row = MAP_H - 1;
mapNewTreasureCol[i] = col;
@ -173,7 +176,8 @@ static void updateMap(){
mapEnemyRow[i] = mapNewRow[i];
if(mapNewCol[i] < 0) continue;
if(mapNewCol[i] == MAP_W / 2 && mapNewRow[i] == pRow) continue;
VDP_setTileMapXY(BG_A, MAP_ENEMY_TILE, MAP_X + mapNewCol[i], MAP_Y + mapNewRow[i]);
u16 eTile = (enemies[i].type == ENEMY_TYPE_BOSS) ? MAP_BOSS_TILE : MAP_ENEMY_TILE;
VDP_setTileMapXY(BG_A, eTile, MAP_X + mapNewCol[i], MAP_Y + mapNewRow[i]);
}
// draw player dot on top
@ -185,29 +189,68 @@ u8 phraseIndex[4];
s16 lastLevel;
static void drawLevel(){
if(isAttract) return;
char lvlStr[4];
uintToStr(level + 1, lvlStr, 1);
VDP_drawText("LVL", 1, 8);
VDP_drawText(lvlStr, 4, 8);
VDP_setTextPalette(hudPal);
// VDP_drawText(lvlStr, 1, 7);
VDP_setTextPalette(PAL0);
lastLevel = level;
}
static void repaintMap(){
VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1);
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(hudPal, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1);
// redraw tracked dots
for(s16 i = 0; i < TREASURE_COUNT; i++)
if(mapTreasureCol[i] >= 0)
VDP_setTileMapXY(BG_A, MAP_TREASURE_TILE, MAP_X + mapTreasureCol[i], MAP_Y + mapTreasureRow[i]);
for(s16 i = 0; i < ENEMY_COUNT; i++)
if(mapEnemyCol[i] >= 0){
u16 eTile = (enemies[i].type == ENEMY_TYPE_BOSS) ? MAP_BOSS_TILE : MAP_ENEMY_TILE;
VDP_setTileMapXY(BG_A, eTile, MAP_X + mapEnemyCol[i], MAP_Y + mapEnemyRow[i]);
}
if(mapPlayerRow >= 0)
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + mapPlayerRow);
}
static void repaintHud(){
bigText(scoreStr, SCORE_X, SCORE_Y, FALSE);
drawLives();
repaintMap();
drawLevel();
}
void loadChrome(){
VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA);
VDP_loadTileSet(imageFontBigger.tileset, FONT_BIG_I, DMA);
VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA);
VDP_loadTileSet(imageChromeLife.tileset, LIFE_I, DMA);
VDP_loadTileSet(imageChromeLife2.tileset, LIFE_I + 2, DMA);
VDP_loadTileSet(mapIndicator.tileset, MAP_I, DMA);
lastScore = 1;
drawScore();
drawLives();
if(!isAttract) drawScore();
if(!isAttract) drawLives();
drawLevel();
}
bool didGameOver;
u32 gameOverClock;
static bool gameOverFading;
static void doGameOver(){
didGameOver = TRUE;
// check and save high score
if(score > highScore){
highScore = score;
saveHighScore();
VDP_drawText("NEW HIGH SCORE!", 14, 15);
}
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active){
@ -238,19 +281,28 @@ static void doGameOver(){
treasureCollectedClock = 0;
allTreasureCollected = FALSE;
hitMessageClock = 0;
VDP_clearText(9, 5, 22);
VDP_clearText(9, 5, 23);
VDP_drawText("GAME OVER", 15, 13);
VDP_drawText("PRESS ANY BUTTON", 12, 14);
hudPal = PAL1;
hudPal = PAL1;
repaintHud();
VDP_drawText("GAME OVER", 15, 14);
VDP_drawText("Press Any Button", 12, 16);
}
#define PAUSE_Y 15
static void showPause(){
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL1);
SPR_setPalette(player.image, PAL1);
hudPal = PAL1;
hudPal = PAL1;
repaintHud();
XGM2_pause();
VDP_drawText("PAUSE", 17, 13);
VDP_drawText("PAUSED", 17, PAUSE_Y);
}
static void clearPause(){
@ -258,13 +310,15 @@ static void clearPause(){
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0);
SPR_setPalette(player.image, PAL0);
hudPal = PAL0;
repaintHud();
XGM2_resume();
VDP_clearText(17, 13, 5);
VDP_clearText(17, PAUSE_Y, 6);
}
u32 pauseClock;
static void updatePause(){
if(gameOver) return;
if(gameOver || isAttract || levelWaitClock > 0 || levelClearing) return;
if(ctrl.start){
if(!isPausing){
isPausing = TRUE;
@ -282,59 +336,90 @@ static void updatePause(){
}
if(paused){
if(pauseClock % 60 < 30)
VDP_drawText("PAUSE", 17, 13);
VDP_drawText("PAUSED", 17, PAUSE_Y);
else
VDP_clearText(17, 13, 5);
VDP_clearText(17, PAUSE_Y, 6);
pauseClock++;
if(pauseClock >= 240) pauseClock = 0;
}
}
#define TRANSITION_TREASURE_X 10
#define TRANSITION_TREASURE_Y 13
#define TRANSITION_LEVEL_X 12
#define TRANSITION_LEVEL_Y 15
void updateChrome(){
updatePause();
if(gameOver && !didGameOver) doGameOver();
if(didGameOver){
gameOverClock++;
if((gameOverClock > 120 && (ctrl.a || ctrl.b || ctrl.c || ctrl.start)) || gameOverClock > 900)
if(!gameOverFading){
if((gameOverClock > 120 && (ctrl.a || ctrl.b || ctrl.c || ctrl.start)) || gameOverClock > 900){
gameOverFading = TRUE;
PAL_fadeOut(0, 47, 20, TRUE);
}
} else if(!PAL_isDoingFade()){
SYS_hardReset();
}
return;
}
// level transition overlay
if(levelClearing){
if(levelClearClock == 2){
char numStr[12];
char lvlStr[4];
uintToStr(level + 2, lvlStr, 1);
VDP_drawText("LEVEL ", 15, 13);
VDP_drawText(lvlStr, 21, 13);
char livesStr[4];
score += 2048 + 1024 * level;
lastScore = score;
uintToStr(statTreasures, numStr, 1);
VDP_drawText("Collected", TRANSITION_TREASURE_X, TRANSITION_TREASURE_Y);
VDP_drawText(numStr, TRANSITION_TREASURE_X + 10, TRANSITION_TREASURE_Y);
VDP_drawText("Treasure", TRANSITION_TREASURE_X + 10 + 2, TRANSITION_TREASURE_Y);
if(statTreasures != 1) VDP_drawText("s", TRANSITION_TREASURE_X + 10 + 2 + 8, TRANSITION_TREASURE_Y);
uintToStr(level + 1, lvlStr, 1);
VDP_drawText("Completed Level", TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y);
VDP_drawText(lvlStr, TRANSITION_LEVEL_X + 16, TRANSITION_LEVEL_Y);
uintToStr(lastScore, scoreStr, 1);
VDP_drawText("Score", TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y + 3);
VDP_drawText(scoreStr, TRANSITION_LEVEL_X + 6, TRANSITION_LEVEL_Y + 3);
uintToStr(player.lives, livesStr, 1);
VDP_drawText(livesStr, TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y + 5);
if(player.lives == 1)
VDP_drawText("Life Left", TRANSITION_LEVEL_X + 2, TRANSITION_LEVEL_Y + 5);
else
VDP_drawText("Lives Left", TRANSITION_LEVEL_X + 2, TRANSITION_LEVEL_Y + 5);
}
if(levelClearClock >= 110){
VDP_clearText(15, 13, 10);
if(levelClearClock >= 230){
VDP_clearText(0, TRANSITION_TREASURE_Y, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 3, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 5, 40);
}
return;
}
if(lastScore != score){
if(!isAttract && lastScore != score){
lastScore = score;
drawScore();
// check for extend
while(score >= nextExtendScore){
player.lives++;
nextExtendScore = (nextExtendScore * 5) / 2; // previous + previous * 1.5
drawLives();
}
}
if(lastLives != player.lives) drawLives();
if(!isAttract && lastLives != player.lives) drawLives();
if(lastLevel != level) drawLevel();
if(treasureCollectedClock > 0 && levelWaitClock == 0){
if(treasureCollectedClock == 120){
VDP_clearText(10, 5, 22);
const char* mirrorPhrases[] = {"REFLECT THE DEPTHS", "DIG DEEPER WITHIN", "SEE WHAT SHINES BELOW", "MIRROR OF THE MINE", "LOOK BACK STRIKE BACK"};
const char* lampPhrases[] = {"STRIKE LIGHT", "LET THERE BE LODE", "BRIGHT IDEA DEEP DOWN", "ILLUMINATE THE VEIN", "GLOW FROM BELOW"};
const char* scarfPhrases[] = {"COZY IN THE CAVES", "WRAP THE UNDERWORLD", "SNUG AS BEDROCK", "STYLE FROM THE STRATA", "WARM THE DEPTHS"};
const char* swordPhrases[] = {"ORE YOU READY", "MINED YOUR STEP", "CUTTING EDGE GEOLOGY", "STRIKE THE VEIN", "SPIRIT STEEL"};
const char** sets[] = {mirrorPhrases, lampPhrases, scarfPhrases, swordPhrases};
const char* phrase = sets[treasureCollectedType][phraseIndex[treasureCollectedType]];
phraseIndex[treasureCollectedType] = (phraseIndex[treasureCollectedType] + 1) % 5;
u8 len = strlen(phrase);
VDP_drawText(phrase, 20 - len / 2, 5);
}
treasureCollectedClock--;
if(treasureCollectedClock == 0){
VDP_clearText(10, 5, 22);
// check if all treasures are collected or gone
VDP_clearText(10, 5, 23);
// check if all treasures are now collected or gone
bool allDone = TRUE;
for(s16 j = 0; j < TREASURE_COUNT; j++){
if(treasures[j].active && treasures[j].state != TREASURE_COLLECTED){
@ -344,26 +429,41 @@ void updateChrome(){
}
if(allDone && collectedCount > 0){
allTreasureCollected = TRUE;
VDP_drawText("ALL TREASURE COLLECTED", 9, 5);
VDP_drawText("All Treasure Found!", 11, 5);
} else {
const char* mirrorPhrases[] = {"Reflect the Depths", "Dig Deeper Within", "See What Shines Below", "Mirror of the Mine", "Look Back, Strike Back"};
const char* lampPhrases[] = {"Strike Light", "Let There Be Lode!", "Bright Idea Deep Down", "Illuminate the Vein", "Glow from Below"};
const char* scarfPhrases[] = {"Cozy in the Caves", "Wrap the Underworld", "Snug as Bedrock", "Style from the Strata", "Warm the Depths"};
const char* swordPhrases[] = {"Ore You Ready?", "Mined Your Step", "Cutting Edge Geology", "Strike the Vein", "Spirit Steel"};
const char** sets[] = {mirrorPhrases, lampPhrases, scarfPhrases, swordPhrases};
const char* phrase = sets[treasureCollectedType][phraseIndex[treasureCollectedType]];
phraseIndex[treasureCollectedType] = (phraseIndex[treasureCollectedType] + 1) % 5;
u8 len = strlen(phrase);
u8 phraseX = 20 - len / 2;
if(phraseX < 10) phraseX = 10;
VDP_drawText(phrase, phraseX, 5);
}
}
treasureCollectedClock--;
if(treasureCollectedClock == 0)
VDP_clearText(9, 5, 24);
}
if(hitMessageClock > 0){
if(hitMessageClock == 120){
VDP_clearText(9, 5, 22);
VDP_clearText(9, 5, 23);
treasureCollectedClock = 0;
allTreasureCollected = FALSE;
VDP_drawText(hitMessageBullet ? "BLASTED" : "SMASHED", hitMessageBullet ? 16 : 16, 5);
VDP_drawText(hitMessageBullet ? "Got You!" : "Collision!", 20 - (hitMessageBullet ? 8 : 10) / 2, 5);
}
hitMessageClock--;
if(hitMessageClock == 0)
VDP_clearText(9, 5, 22);
VDP_clearText(9, 5, 23);
}
if(levelWaitClock == 240){
VDP_clearText(9, 5, 22);
if(levelWaitClock == 210){
VDP_clearText(9, 5, 23);
treasureCollectedClock = 0;
allTreasureCollected = FALSE;
VDP_drawText("ALL ENEMIES DESTROYED", 9, 5);
VDP_drawText("All Enemies Down!", 12, 5);
}
if(clock % 4 == 0) updateMap();
}

View file

@ -42,19 +42,27 @@ void spawnEnemy(u8 type, u8 zone){
enemies[i].pos.x = randX;
enemies[i].pos.y = randY;
enemies[i].off = 16;
enemies[i].image = SPR_addSprite(&fairySprite,
getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, fix32ToInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0));
static const SpriteDefinition* bossSpriteDefs[4] = { &boss1Sprite, &boss2Sprite, &boss3Sprite, &boss4Sprite };
SpriteDefinition const* spriteDef;
if(type == ENEMY_TYPE_DRONE) spriteDef = &eyeBigSprite;
else if(type == ENEMY_TYPE_BOSS) spriteDef = bossSpriteDefs[pendingBossNum % 4];
else spriteDef = &fairySprite;
enemies[i].off = (type == ENEMY_TYPE_BOSS) ? 24 : 16;
enemies[i].image = SPR_addSprite(spriteDef,
getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, F32_toInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0));
if(!enemies[i].image){
enemies[i].active = FALSE;
return;
}
SPR_setDepth(enemies[i].image, (type == ENEMY_TYPE_BOSS) ? 1 : 2);
SPR_setVisibility(enemies[i].image, HIDDEN);
enemies[i].hp = 1;
for(u8 j = 0; j < PROP_COUNT; j++){
enemies[i].ints[j] = 0;
enemies[i].fixes[j] = 0;
}
enemies[i].ints[3] = -1;
enemies[i].anim = 0;
switch(enemies[i].type){
case ENEMY_TYPE_TEST:
loadEnemyOne(i);
@ -75,9 +83,9 @@ void spawnEnemy(u8 type, u8 zone){
loadBoss(i);
break;
}
enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed);
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed);
enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
SPR_setAnim(enemies[i].image, enemies[i].anim);
}
static void boundsEnemy(u8 i){
@ -92,21 +100,39 @@ static void boundsEnemy(u8 i){
}
// carrying: only check for reaching the top
else if(enemies[i].pos.y <= FIX32(0)){
if(treasures[h].active) killTreasure(h);
enemies[i].ints[3] = -1;
treasureBeingCarried = FALSE;
if(enemies[i].type == ENEMY_TYPE_BUILDER){
u8 zone = fix32ToInt(enemies[i].pos.x) / 512;
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
if(isAttract){
// in attract mode enemies can't die -- drop treasure and head back down
if(treasures[h].active){
treasures[h].state = TREASURE_FALLING;
treasures[h].carriedBy = -1;
treasures[h].vel.x = 0;
treasures[h].vel.y = FIX32(3);
}
enemies[i].ints[3] = -1;
treasureBeingCarried = FALSE;
enemies[i].vel.y = FIX32(1);
} else {
if(treasures[h].active) killTreasure(h);
enemies[i].ints[3] = -1;
treasureBeingCarried = FALSE;
if(enemies[i].type == ENEMY_TYPE_BUILDER){
u8 zone = F32_toInt(enemies[i].pos.x) / 512;
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
}
enemies[i].hp = 0;
killEnemy(i);
}
enemies[i].hp = 0;
killEnemy(i);
return;
}
} else {
// not carrying: bounce off top and bottom
if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off) || enemies[i].pos.y <= FIX32(enemies[i].off))
enemies[i].vel.y *= -1;
if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off)){
if(enemies[i].vel.y > 0) enemies[i].vel.y = -enemies[i].vel.y;
enemies[i].pos.y = GAME_H_F - FIX32(enemies[i].off);
} else if(enemies[i].pos.y <= FIX32(enemies[i].off)){
if(enemies[i].vel.y < 0) enemies[i].vel.y = -enemies[i].vel.y;
enemies[i].pos.y = FIX32(enemies[i].off);
}
}
if(enemies[i].pos.x >= GAME_WRAP){
@ -118,8 +144,8 @@ static void boundsEnemy(u8 i){
}
static void updateEnemy(u8 i){
enemies[i].pos.x += enemies[i].vel.x;
enemies[i].pos.y += enemies[i].vel.y;
enemies[i].pos.x += enemies[i].vel.x - (player.vel.x >> 3);
enemies[i].pos.y += enemies[i].vel.y - (playerScrollVelY >> 3);
boundsEnemy(i);
if(!enemies[i].active) return;
@ -169,6 +195,7 @@ static void updateEnemy(u8 i){
bullets[expSlot].frame = 0;
bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0));
if(bullets[expSlot].image){
SPR_setDepth(bullets[expSlot].image, 5);
SPR_setAnim(bullets[expSlot].image, 1);
SPR_setFrame(bullets[expSlot].image, 0);
SPR_setHFlip(bullets[expSlot].image, random() & 1);
@ -180,24 +207,29 @@ static void updateEnemy(u8 i){
enemies[i].hp = 0;
killEnemy(i);
}
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
} else {
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = FALSE;
if(!isAttract){
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
sfxGameOver();
} else {
sfxPlayerHit();
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = FALSE;
}
}
}
}
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
s16 sy = fix32ToInt(enemies[i].pos.y);
s16 sy = F32_toInt(enemies[i].pos.y);
SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN);
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0);
if(enemies[i].type != ENEMY_TYPE_DRONE && enemies[i].type != ENEMY_TYPE_BOSS)
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0);
SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off);
enemies[i].clock++;

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ void sfxEnemyShotB();
void sfxEnemyShotC();
void sfxExplosion();
void sfxPickup();
void sfxGraze();
void loadMap();
void loadGame();
@ -21,12 +22,34 @@ u32 clock;
#define GAME_WRAP (SECTION_SIZE * SECTION_COUNT)
#define CULL_LIMIT FIX32(240)
#define SCREEN_LIMIT FIX32(208) // max player-to-screen-edge distance (320 - CAMERA_X)
#define MUSIC_VOLUME 50
// #define MUSIC_VOLUME 0
// #define MUSIC_VOLUME 50
#define MUSIC_VOLUME 0
u32 score;
u32 highScore;
u32 tempHighScore;
u32 grazeCount;
u32 nextExtendScore;
#define EXTEND_SCORE 25000
#define SCORE_LENGTH 8
#define GRAZE_RADIUS 16
#define SCORE_SRAM 0x0033
void getHighScore(){
SRAM_enable();
tempHighScore = SRAM_readLong(SCORE_SRAM);
if(tempHighScore > 0 && tempHighScore < 1000000) highScore = tempHighScore;
SRAM_disable();
}
static void saveHighScore(){
SRAM_enable();
SRAM_writeLong(SCORE_SRAM, highScore);
SRAM_disable();
}
#define FIRST_ROTATING_BULLET 3
@ -34,12 +57,20 @@ u32 score;
#define MAP_Y 1
#define MAP_W 38
#define MAP_H 3
#define MAP_SCALE (F32_toInt(GAME_WRAP) / MAP_W)
void EMPTY(s16 i){(void)i;}
bool started;
bool gameOver;
bool paused, isPausing;
bool isAttract;
u16 attractClock;
#define ATTRACT_LIMIT 900 // frames of title idle before attract triggers
#define ATTRACT_DURATION 1800 // 30 seconds of demo gameplay
#define ATTRACT_LEVEL 1 // level index for attract mode (L12: Boss 4, 3 gunners)
#define START_LEVEL 2 // offset added to starting level (0 = normal start)
// #define START_LEVEL 0 // offset added to starting level (0 = normal start)
s16 enemyCount, bulletCount;
u8 level;
s16 pendingBossHp;
@ -59,6 +90,7 @@ struct controls {
bool left, right, up, down, a, b, c, start;
};
struct controls ctrl;
struct controls ctrlHW; // hardware-only copy — never overridden by AI
void updateControls(u16 joy, u16 changed, u16 state){
(void)changed; // Unused parameter
if(joy == JOY_1){
@ -70,6 +102,7 @@ void updateControls(u16 joy, u16 changed, u16 state){
ctrl.b = (state & BUTTON_B);
ctrl.c = (state & BUTTON_C);
ctrl.start = (state & BUTTON_START);
ctrlHW = ctrl;
}
}
@ -79,10 +112,13 @@ struct playerStruct {
Vect2D_f32 pos, vel;
s16 shotAngle;
u8 lives, recoveringClock, respawnClock;
bool recoverFlash; // TRUE only after death, not on level-start grace
bool pendingShow; // show sprite after next position update (avoids 1-frame position flicker)
fix32 camera;
Sprite* image;
};
struct playerStruct player;
fix32 playerScrollVelY; // player.vel.y zeroed when clamped at top/bottom bound
bool killBullets;
@ -93,14 +129,17 @@ struct bulletSpawner {
fix32 x, y, speed;
Vect2D_f32 vel;
s16 angle, anim, frame;
s16 ints[PROP_COUNT];
bool top, player;
};
struct bullet {
bool active, player, vFlip, hFlip, explosion;
fix32 speed;
bool active, player, vFlip, hFlip, explosion, grazed;
Vect2D_f32 pos, vel;
Sprite* image;
s16 clock, angle, anim, frame;
s16 dist;
s16 ints[PROP_COUNT];
void (*updater)(s16);
};
struct bullet bullets[BULLET_COUNT];
@ -119,13 +158,14 @@ struct bullet bullets[BULLET_COUNT];
struct enemy {
bool active, onScreen;
u8 type;
s16 hp;
s16 hp, frame, anim;
s16 angle, off;
u32 clock;
fix32 speed;
Vect2D_f32 vel, pos;
Sprite* image;
s16 ints[PROP_COUNT];
fix16 fixes[PROP_COUNT];
};
struct enemy enemies[ENEMY_COUNT];
@ -148,6 +188,9 @@ struct treasure {
struct treasure treasures[TREASURE_COUNT];
bool treasureBeingCarried;
s16 collectedCount;
u16 levelEnemiesKilled;
u16 statEnemiesKilled;
s16 statTreasures;
void killTreasure(u8 i){
if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){
@ -160,19 +203,17 @@ void killTreasure(u8 i){
void killBullet(u8 i, bool explode){
if(explode){
s16 a = bullets[i].anim;
s16 explosionAnim;
if(bullets[i].player){
SPR_setAnim(bullets[i].image, 1);
explosionAnim = 16;
} else if(a < FIRST_ROTATING_BULLET){
explosionAnim = 13 + bullets[i].frame;
} else {
s16 a = bullets[i].anim;
s16 explosionAnim;
if(a < FIRST_ROTATING_BULLET){
explosionAnim = 13 + bullets[i].frame;
} else {
s16 mod = a % 3;
explosionAnim = 13 + mod;
}
SPR_setAnim(bullets[i].image, explosionAnim);
s16 mod = a % 3;
explosionAnim = 13 + mod;
}
SPR_setAnim(bullets[i].image, explosionAnim);
bullets[i].clock = 0;
bullets[i].frame = 0;
bullets[i].explosion = TRUE;
@ -186,6 +227,7 @@ void killBullet(u8 i, bool explode){
}
void killEnemy(u8 i){
if(isAttract) return;
enemies[i].hp--;
if(enemies[i].hp > 0) return;
if(enemies[i].ints[3] >= 0){
@ -200,6 +242,7 @@ void killEnemy(u8 i){
}
enemies[i].active = FALSE;
SPR_releaseSprite(enemies[i].image);
levelEnemiesKilled++;
}
static fix32 getWrappedDelta(fix32 a, fix32 b) {
@ -214,37 +257,61 @@ static fix32 getWrappedDelta(fix32 a, fix32 b) {
static s16 getScreenX(fix32 worldX, fix32 camera) {
fix32 screenX = worldX - camera;
if (screenX < FIX32(-256)) {
if (screenX < -(GAME_WRAP / 2)) {
screenX += GAME_WRAP;
} else if (screenX > FIX32(256)) {
} else if (screenX > (GAME_WRAP / 2)) {
screenX -= GAME_WRAP;
}
return fix32ToInt(screenX);
return F32_toInt(screenX);
}
// homing
#define PI_MOD 2.84444444444
#define PI_F FIX16(3.14159265358 * PI_MOD)
#define PI_F_2 FIX16(1.57079632679 * PI_MOD)
#define PI_F_4 FIX16(0.78539816339 * PI_MOD)
fix16 arctan(fix16 x) {
return fix16Mul(PI_F_4, x) - fix16Mul(fix16Mul(x, (abs(x) - 1)), (FIX16(0.245) + fix16Mul(FIX16(0.066), abs(x))));
// homing -- degree-based using SGDK F16_atan2 (returns fix16 degrees)
static fix16 getAngle(fix32 dx, fix32 dy){
s16 ix = (s16)(F32_toInt(dx) >> 2);
s16 iy = (s16)(F32_toInt(dy) >> 2);
if(ix == 0 && iy == 0) return 0;
return F16_normalizeAngle(F16_atan2(FIX16(iy), FIX16(ix)));
}
fix16 arctan2(fix16 y, fix16 x) {
return x >= 0 ?
(y >= 0 ? (y < x ? arctan(fix16Div(y, x)) : PI_F_2 - arctan(fix16Div(x, y))) : (-y < x ? arctan(fix16Div(y, x)) : -PI_F_2 - arctan(fix16Div(x, y)))) :
(y >= 0 ? (y < -x ? arctan(fix16Div(y, x)) + PI_F : PI_F_2 - arctan(fix16Div(x, y))) : (-y < -x ? arctan(fix16Div(y, x)) - PI_F : -PI_F_2 - arctan(fix16Div(x, y))));
// safe angle accumulation -- keeps angle in [-180, 180) so adding up to 180° can't overflow s16
static s16 angleAdd(s16 a, s16 step){
if(a >= FIX16(180)) a -= FIX16(360);
return a + step;
}
s16 arcAngle;
s16 honeAngle(fix16 x1, fix16 x2, fix16 y1, fix16 y2){
arcAngle = arctan2(y2 - y1, x2 - x1);
if(arcAngle >= 128) arcAngle -= 32;
if(arcAngle >= 384) arcAngle -= 32;
if(arcAngle < 0){
arcAngle = 1024 + arcAngle;
if(arcAngle < 896) arcAngle += 32;
if(arcAngle < 640) arcAngle += 32;
static bool isBossLevel(u8 lvl){
return (lvl >= 2) && ((lvl + 1) % 3 == 0);
}
#define FONT_THEME_RED 0
#define FONT_THEME_GREEN 1
#define FONT_THEME_BLUE 2
u16 fontPal[16];
void loadFontPalette(u8 theme) {
u16 coloredPalette[16];
u8 i;
for(i = 0; i < 16; i++) {
u16 color = font.palette->data[i];
u16 r = color & 0xF;
u16 g = (color >> 4) & 0xF;
u16 b = (color >> 8) & 0xF;
switch(theme) {
case FONT_THEME_GREEN:
coloredPalette[i] = (b << 8) | (r << 4) | g;
break;
case FONT_THEME_BLUE: {
u16 newB = r > b ? r : b;
coloredPalette[i] = (newB << 8) | (g << 4) | (r >> 1);
break;
}
default: // FONT_THEME_RED
coloredPalette[i] = color;
break;
}
}
return arcAngle;
memcpy(fontPal, coloredPalette, 16 * sizeof(u16));
PAL_setPalette(PAL3, coloredPalette, DMA_QUEUE);
VDP_setTextPalette(PAL3);
}

View file

@ -10,19 +10,32 @@
#include "stage.h"
#include "chrome.h"
#include "start.h"
#include "starfield.h"
#include "sfx.h"
static void loadInternals(){
JOY_init();
JOY_setEventHandler(&updateControls);
SPR_init();
VDP_setPlaneSize(128, 32, TRUE);
SPR_init();
VDP_loadFont(font.tileset, DMA);
PAL_setPalette(PAL0, font.palette->data, DMA);
PAL_setPalette(PAL1, shadow.palette->data, CPU);
PAL_setPalette(PAL2, shadow.palette->data, CPU);
VDP_setTextPriority(1);
}
static bool attractEnding;
static void startLevelFadeIn(){
u16 ft[48];
memcpy(ft, font.palette->data, 16 * sizeof(u16));
memcpy(ft + 16, shadow.palette->data, 16 * sizeof(u16));
memcpy(ft + 32, bgPal, 16 * sizeof(u16));
PAL_setColors(0, palette_black, 48, CPU);
PAL_fadeIn(0, 47, ft, 20, TRUE);
}
void clearLevel(){
for(s16 i = 0; i < BULLET_COUNT; i++)
if(bullets[i].active) killBullet(i, FALSE);
@ -34,6 +47,7 @@ void clearLevel(){
collectedCount = 0;
allTreasureCollected = FALSE;
treasureCollectedClock = 0;
levelEnemiesKilled = 0;
// black out everything
SPR_setVisibility(player.image, HIDDEN);
VDP_clearTileMapRect(BG_A, 0, 0, 128, 32);
@ -41,43 +55,90 @@ void clearLevel(){
}
void loadGame(){
VDP_setVerticalScroll(BG_A, 0);
score = 0;
nextExtendScore = EXTEND_SCORE;
loadBackground();
loadPlayer();
loadChrome();
loadLevel(0);
XGM2_play(stageMusic);
XGM2_setFMVolume(MUSIC_VOLUME);
XGM2_setPSGVolume(MUSIC_VOLUME);
loadLevel(isAttract ? ATTRACT_LEVEL : START_LEVEL);
#if MUSIC_VOLUME > 0
if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic);
#endif
player.recoveringClock = 240;
player.recoverFlash = FALSE;
killBullets = TRUE;
attractEnding = FALSE;
started = TRUE;
startLevelFadeIn();
}
static void updateGame(){
if(isAttract){
if(!attractEnding){
if(ctrlHW.start || ctrlHW.a || ctrlHW.b || ctrlHW.c){
attractEnding = TRUE;
PAL_fadeOut(0, 47, 20, TRUE);
}
if(attractClock > 0){
attractClock--;
if(attractClock == 20)
PAL_fadeOut(0, 47, 20, TRUE);
} else {
SYS_hardReset();
}
} else if(!PAL_isDoingFade()){
SYS_hardReset();
}
}
updateChrome();
updateSfx();
if(levelClearing){
levelClearClock++;
if(levelClearClock == 73)
XGM2_stop();
if(levelClearClock == 1){
clearLevel();
loadStarfield(level % 3);
u16 transPal[32];
memcpy(transPal, font.palette->data, 16 * sizeof(u16));
memcpy(transPal + 16, shadow.palette->data, 16 * sizeof(u16));
PAL_fadeIn(0, 31, transPal, 20, TRUE);
}
if(levelClearClock >= 120){
if(levelClearClock == 220)
PAL_fadeOut(0, 31, 20, TRUE);
if(levelClearClock >= 240){
levelClearing = FALSE;
player.pos.y = FIX32(112);
player.camera = player.pos.x - FIX32(160);
playerVelX = 0;
clearStarfield();
loadBackground();
loadChrome();
loadLevel(level + 1);
SPR_setVisibility(player.image, VISIBLE);
startLevelFadeIn();
player.pendingShow = TRUE;
player.recoveringClock = 240;
player.recoverFlash = FALSE;
killBullets = TRUE;
XGM2_stop();
#if MUSIC_VOLUME > 0
if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic);
#endif
}
if(levelClearing) updateStarfield();
return;
}
if(levelWaitClock > 0){
levelWaitClock--;
#if MUSIC_VOLUME > 0
if(levelWaitClock == 209 && !isAttract)
XGM2_play(treasureMusic);
#endif
if(levelWaitClock == 20)
PAL_fadeOut(0, 47, 20, TRUE);
if(levelWaitClock == 0){
statEnemiesKilled = levelEnemiesKilled;
statTreasures = collectedCount;
levelClearing = TRUE;
levelClearClock = 0;
return;
@ -93,8 +154,10 @@ static void updateGame(){
gameOver = TRUE;
XGM2_stop();
} else {
levelWaitClock = 240;
levelWaitClock = 210;
killBullets = TRUE;
if(paused){ paused = FALSE; clearPause(); }
XGM2_stop();
}
}
updateTreasures();

View file

@ -1,4 +1,4 @@
#define PLAYER_SPEED FIX32(6)
#define PLAYER_SPEED FIX32(5.5)
#define PLAYER_ACCEL PLAYER_SPEED >> 4
@ -6,16 +6,17 @@
#define PLAYER_BOUND_Y FIX32(PLAYER_OFF)
#define PLAYER_BOUND_H FIX32(224 - PLAYER_OFF)
#define CAMERA_X FIX32(96)
#define CAMERA_W FIX32(224)
#define CAMERA_X FIX32(112)
#define CAMERA_W FIX32(208)
#define SHOT_INTERVAL 15
#define SHOT_INTERVAL 20
#define PLAYER_SHOT_SPEED FIX32(18)
s16 shotClock;
fix32 screenX;
fix32 playerSpeed;
fix32 playerVelX;
fix32 playerVelX;
static void movePlayer(){
player.vel.y = 0;
@ -24,7 +25,7 @@ static void movePlayer(){
if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){
playerSpeed = PLAYER_SPEED;
if(ctrl.left || ctrl.right){
if(!ctrl.a) player.shotAngle = ctrl.left ? 512 : 0;
if(!ctrl.a) player.shotAngle = ctrl.left ? FIX16(180) : 0;
targetVelX = ctrl.left ? -playerSpeed : playerSpeed;
}
@ -43,8 +44,8 @@ static void movePlayer(){
player.vel.x = playerVelX;
if(player.vel.x != 0 && player.vel.y != 0){
player.vel.x = fix32Mul(player.vel.x, FIX32(0.707));
player.vel.y = fix32Mul(player.vel.y, FIX32(0.707));
player.vel.x = F32_mul(player.vel.x, FIX32(0.707));
player.vel.y = F32_mul(player.vel.y, FIX32(0.707));
}
player.pos.x += player.vel.x;
@ -62,10 +63,14 @@ static void movePlayer(){
}
static void boundsPlayer(){
if(player.pos.y < PLAYER_BOUND_Y)
playerScrollVelY = player.vel.y;
if(player.pos.y < PLAYER_BOUND_Y){
player.pos.y = PLAYER_BOUND_Y;
else if(player.pos.y > PLAYER_BOUND_H)
if(playerScrollVelY < 0) playerScrollVelY = 0;
} else if(player.pos.y > PLAYER_BOUND_H){
player.pos.y = PLAYER_BOUND_H;
if(playerScrollVelY > 0) playerScrollVelY = 0;
}
if(player.pos.x >= GAME_WRAP){
player.pos.x -= GAME_WRAP;
@ -89,15 +94,18 @@ static void cameraPlayer(){
static void shootPlayer(){
if(ctrl.a && shotClock == 0){
// fix32 bulletVelX = (player.shotAngle == 0 ? PLAYER_SHOT_SPEED : -PLAYER_SHOT_SPEED) + (player.vel.x * 3);
struct bulletSpawner spawner = {
.x = player.pos.x,
.y = player.pos.y,
.anim = 0,
.speed = FIX32(24),
.anim = 12,
.speed = PLAYER_SHOT_SPEED,
.angle = player.shotAngle,
.player = TRUE
};
spawner.ints[5] = F32_toInt(player.shotAngle == 0 ? PLAYER_SHOT_SPEED : -PLAYER_SHOT_SPEED);
void updater(s16 i){
bullets[i].vel.x = FIX32(bullets[i].ints[5]) + (player.vel.x << 2);
if(bullets[i].clock == 4) killBullet(i, TRUE);
}
spawnBullet(spawner, updater);
@ -106,6 +114,57 @@ static void shootPlayer(){
} else if(shotClock > 0) shotClock--;
}
static s16 attractXClock = 0;
static s16 attractXState = 0; // 0=moving, 1=paused
static s16 attractXDir = 1; // 1=right, -1=left
static s16 attractYClock = 0;
static s16 attractTargetY = 112;
static void moveAttract(){
ctrl.left = ctrl.right = ctrl.up = ctrl.down = FALSE;
// X: move toward nearest enemy, pause, repeat
attractXClock--;
if(attractXClock <= 0){
if(attractXState == 0){
// finished moving -- pause
attractXState = 1;
attractXClock = 20 + (random() % 30); // pause 20-49 frames
} else {
// finished pausing -- find nearest enemy and head toward it
attractXState = 0;
attractXClock = 55 + (random() % 55); // move 55-109 frames
attractXDir = 1; // default right if no enemies
fix32 bestDist = FIX32(9999);
for(s16 i = 0; i < ENEMY_COUNT; i++){
if(!enemies[i].active) continue;
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
fix32 dist = dx < 0 ? -dx : dx;
if(dist < bestDist){
bestDist = dist;
attractXDir = (dx >= 0) ? 1 : -1;
}
}
}
}
ctrl.right = (attractXState == 0 && attractXDir == 1);
ctrl.left = (attractXState == 0 && attractXDir == -1);
// Y: pick a new target every 60 frames, steer toward it with a soft dead zone
// Range [48, 176] gives 128px of vertical movement
attractYClock++;
if(attractYClock >= 60){
attractYClock = 0;
attractTargetY = 48 + (random() % 128);
}
fix32 dy = FIX32(attractTargetY) - player.pos.y;
ctrl.down = (dy > FIX32(10));
ctrl.up = (dy < FIX32(-10));
// Auto-shoot every few shot intervals
ctrl.a = ((clock % (SHOT_INTERVAL * 3)) < SHOT_INTERVAL);
}
void loadPlayer(){
player.shotAngle = 0;
player.camera = 0;
@ -113,10 +172,12 @@ void loadPlayer(){
player.pos.y = FIX32(112);
playerVelX = 0;
player.lives = 3;
shotClock = isAttract ? SHOT_INTERVAL : 0;
player.image = SPR_addSprite(&momoyoSprite,
fix32ToInt(player.pos.x) - PLAYER_OFF,
fix32ToInt(player.pos.y) - PLAYER_OFF,
F32_toInt(player.pos.x) - PLAYER_OFF,
F32_toInt(player.pos.y) - PLAYER_OFF,
TILE_ATTR(PAL0, 0, 0, 0));
SPR_setDepth(player.image, 0);
}
void updatePlayer(){
@ -140,29 +201,35 @@ void updatePlayer(){
player.pos.y += (targetY - player.pos.y) >> 3;
// keep sprite position in sync so it doesn't pop on reappear
s16 sx = getScreenX(player.pos.x, player.camera);
s16 sy = fix32ToInt(player.pos.y);
s16 sy = F32_toInt(player.pos.y);
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
player.respawnClock--;
if(player.respawnClock == 0){
SPR_setVisibility(player.image, VISIBLE);
player.recoveringClock = 240;
player.recoveringClock = 180;
player.recoverFlash = TRUE;
killBullets = TRUE;
}
return;
}
if(player.recoveringClock > 0){
if(player.recoveringClock % 10 == 1)
if(player.recoverFlash && player.recoveringClock % 10 == 1)
SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN);
player.recoveringClock--;
if(player.recoveringClock == 0)
SPR_setVisibility(player.image, VISIBLE);
}
if(isAttract) moveAttract();
movePlayer();
boundsPlayer();
cameraPlayer();
shootPlayer();
s16 sx = getScreenX(player.pos.x, player.camera);
s16 sy = fix32ToInt(player.pos.y);
s16 sy = F32_toInt(player.pos.y);
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
if(player.pendingShow){
SPR_setVisibility(player.image, VISIBLE);
player.pendingShow = FALSE;
}
}
}

125
src/sfx.h
View file

@ -1,105 +1,58 @@
static s16 sfxShotClock;
static u16 sfxShotFreq;
#define SFX_VOL 2
void sfxPlayerShot(){
sfxShotClock = 4;
sfxShotFreq = 150;
PSG_setEnvelope(2, SFX_VOL);
PSG_setFrequency(2, sfxShotFreq);
XGM2_playPCM(sfxSamplePlayerShot, sizeof(sfxSamplePlayerShot), SOUND_PCM_CH1);
}
static s16 sfxEnemyShotClock;
static u16 sfxEnemyShotFreq;
static u8 sfxEnemyShotType;
// high sharp zap - quick descending chirp
void sfxEnemyShotA(){
if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 3;
sfxEnemyShotFreq = 1200;
sfxEnemyShotType = 0;
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
XGM2_playPCM(sfxSampleBullet1, sizeof(sfxSampleBullet1), SOUND_PCM_CH2);
}
// mid buzzy pulse - sits in the midrange
void sfxEnemyShotB(){
if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 5;
sfxEnemyShotFreq = 400;
sfxEnemyShotType = 1;
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
XGM2_playPCM(sfxSampleBullet2, sizeof(sfxSampleBullet2), SOUND_PCM_CH2);
}
// quick rising ping - sweeps upward
void sfxEnemyShotC(){
if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 4;
sfxEnemyShotFreq = 300;
sfxEnemyShotType = 2;
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
XGM2_playPCM(sfxSampleBullet3, sizeof(sfxSampleBullet3), SOUND_PCM_CH2);
}
static s16 sfxPickupClock;
static u16 sfxPickupFreq;
void sfxPickup(){
sfxPickupClock = 12;
sfxPickupFreq = 800;
PSG_setEnvelope(0, SFX_VOL);
PSG_setFrequency(0, sfxPickupFreq);
}
static s16 sfxExpClock;
void sfxExplosion(){
sfxExpClock = 18;
PSG_setNoise(PSG_NOISE_TYPE_WHITE, PSG_NOISE_FREQ_CLOCK2);
PSG_setEnvelope(3, SFX_VOL);
XGM2_playPCM(sfxSampleExplosion, sizeof(sfxSampleExplosion), SOUND_PCM_CH3);
}
void updateSfx(){
if(sfxExpClock > 0){
sfxExpClock--;
PSG_setEnvelope(3, SFX_VOL + (18 - sfxExpClock) * (15 - SFX_VOL) / 18);
if(sfxExpClock == 0){
PSG_setEnvelope(3, 15);
}
}
if(sfxEnemyShotClock > 0){
sfxEnemyShotClock--;
if(sfxEnemyShotType == 0) sfxEnemyShotFreq -= 300;
else if(sfxEnemyShotType == 1) sfxEnemyShotFreq -= 50;
else sfxEnemyShotFreq += 150;
PSG_setFrequency(1, sfxEnemyShotFreq);
PSG_setEnvelope(1, SFX_VOL + (sfxEnemyShotType == 0 ? (3 - sfxEnemyShotClock) * (15 - SFX_VOL) / 3 :
sfxEnemyShotType == 1 ? (5 - sfxEnemyShotClock) * (15 - SFX_VOL) / 5 :
(4 - sfxEnemyShotClock) * (15 - SFX_VOL) / 4));
if(sfxEnemyShotClock == 0){
PSG_setEnvelope(1, 15);
}
}
if(sfxPickupClock > 0){
sfxPickupClock--;
// rising staircase: jump up every 3 frames
if(sfxPickupClock % 3 == 0) sfxPickupFreq += 200;
PSG_setFrequency(0, sfxPickupFreq);
PSG_setEnvelope(0, SFX_VOL);
if(sfxPickupClock == 0){
PSG_setEnvelope(0, 15);
}
}
if(sfxShotClock > 0){
sfxShotClock--;
sfxShotFreq -= 30;
PSG_setFrequency(2, sfxShotFreq);
PSG_setEnvelope(2, SFX_VOL + (4 - sfxShotClock) * (15 - SFX_VOL) / 4);
if(sfxShotClock == 0){
PSG_setEnvelope(2, 15);
}
}
void sfxPickup(){
XGM2_playPCM(sfxSamplePickup, sizeof(sfxSamplePickup), SOUND_PCM_CH1);
}
void sfxGraze(){
XGM2_playPCM(sfxSampleGraze, sizeof(sfxSampleGraze), SOUND_PCM_CH1);
}
void sfxStartGame(){
XGM2_playPCM(sfxSampleStartGame, sizeof(sfxSampleStartGame), SOUND_PCM_CH1);
}
void sfxPlayerHit(){
XGM2_playPCM(sfxSamplePlayerHit, sizeof(sfxSamplePlayerHit), SOUND_PCM_CH3);
}
void sfxGameOver(){
XGM2_playPCM(sfxSampleGameOver, sizeof(sfxSampleGameOver), SOUND_PCM_CH1);
}
void sfxMenuSelect(){
XGM2_playPCM(sfxSampleMenuSelect, sizeof(sfxSampleMenuSelect), SOUND_PCM_CH2);
}
void sfxMenuChoose(){
XGM2_playPCM(sfxSampleMenuChoose, sizeof(sfxSampleMenuChoose), SOUND_PCM_CH2);
}
void sfxCollectTreasure(){
XGM2_playPCM(sfxSampleCollectTreasure, sizeof(sfxSampleCollectTreasure), SOUND_PCM_CH3);
}
void sfxCollectAllTreasures(){
XGM2_playPCM(sfxSampleCollectAllTreasures, sizeof(sfxSampleCollectAllTreasures), SOUND_PCM_CH3);
}

View file

@ -1,176 +1,191 @@
// =============================================================================
// LEVEL DESIGN GUIDE
// THREAT POINT LEVEL GENERATION SYSTEM
// =============================================================================
//
// Each level is a single struct defining what spawns. The level ends when all
// enemies are killed (enemyCount == 0). Treasures are bonus -- they don't affect
// level completion.
// Levels are procedurally generated using a threat point (TP) budget.
// Each enemy type has a TP cost, and the budget grows with level index.
// Enemy types unlock progressively. Compositions vary each playthrough
// (RNG seeded from title screen).
//
// --- STRUCT FIELDS ---
//
// drones Number of Drone enemies (type 1). Bulk pressure enemy.
// Speed 2, homes toward player every 30 frames.
// Fires 1 aimed bullet every 40 frames (only on L2+, i.e. index >= 1).
// Use dronesShoot=FALSE on L1 to introduce them without bullets.
// Good range: 6-16. Above 14 gets chaotic.
//
// gunners Number of Gunner enemies (type 2). Danmaku / bullet geometry.
// Speed 0.5 (slow drift), only shoots when on screen.
// Pattern controlled by gunnerPattern field (see below).
// Good range: 0-6. Even 2-3 gunners create significant bullet density.
//
// hunters Number of Hunter enemies (type 3). Fast chaser, no shooting.
// Speed 5 (matches player!). Homes every frame. Pure body pressure.
// Very dangerous -- 2-3 is oppressive, 6 is near-impossible.
// Good range: 0-6. Introduce after players learn movement.
//
// builders Number of Builder enemies (type 4). Treasure abductor.
// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking treasures
// and flies upward. If it reaches the top, the treasure dies and a
// Gunner spawns in its place. Only 1 treasure can be carried at a time.
// Creates urgency -- player must choose between killing enemies
// and saving treasures. Good range: 0-2.
//
// bossHp If > 0, spawns a Boss enemy (type 5) with this many HP.
// Boss number is auto-calculated from level index (lvl / 4).
// Set to 0 for non-boss levels. Boss speed is 1, bounces around.
// Boss has multiple attack phases based on remaining HP.
// Typical values: 25, 50, 75, 100, 125.
// Other enemies can coexist with the boss (adds pressure).
//
// treasures Number of treasures to spawn. Distributed across 4 zones
// (2 per zone if >= 4 treasures, then 1 each for remainder).
// Walk along the ground, can be collected by player for 1000 pts
// (2000 if caught mid-fall after enemy drops them).
// Max 8 (TREASURE_COUNT). Usually just set to 8.
//
// gunnerPattern Controls what bullet pattern gunners use:
// 0 = Radial Burst: 8 bullets in a circle every 60 frames.
// Rotating start angle. Steady, predictable pressure.
// 1 = Aimed Fan: 5 bullets aimed at player, spread +-64,
// every 45 frames. More targeted/aggressive.
// 2 = Mix: each gunner randomly picks 0 or 1 at spawn.
// Creates varied, less predictable bullet fields.
//
// dronesShoot TRUE = drones fire aimed bullets (normal behavior on L2+).
// FALSE = drones still home toward player but never shoot.
// Only meaningful for the very first level as a gentle intro.
// (Drone shooting is also gated by level >= 1 in code, so
// L1 drones never shoot regardless of this flag.)
//
// --- LIMITS ---
//
// Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss
// must not exceed 24. If it does, excess enemies silently fail to spawn.
//
// Total treasures: 8 slots (TREASURE_COUNT).
//
// Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets
// get priority and evict enemy bullets when full.
//
// --- SPAWNING ---
//
// Enemies are distributed across 4 zones (each 512px of the 2048px world).
// Enemy i spawns in zone (i % 4). They never spawn within 240px of the player
// and maintain 64px minimum spacing from each other.
//
// Boss always spawns in zone 1.
//
// --- DESIGN TIPS ---
//
// - Drone-heavy levels (12-16) create constant movement pressure
// - Gunner-heavy levels (4-6) create bullet reading / dodging challenges
// - Hunter levels force the player to keep moving (anti-camping)
// - Builder levels force tough choices: kill builders or save treasures?
// - Combining hunters + gunners is very hard (dodge bullets while fleeing)
// - Boss levels with escort enemies (drones/gunners alongside boss) are
// harder than solo boss fights
// - A "farm" level (lots of drones, no gunners) gives score-building breathers
// - gunnerPattern 0 (radial) is easier to dodge than 1 (aimed fan)
// Boss levels occur every 3rd level (indices 2, 5, 8, 11, 14).
// Boss escorts use 40% of normal budget, limited to drones + builders.
//
// =============================================================================
struct LevelDef {
u8 drones, gunners, hunters, builders;
u8 bossHp;
u8 treasures;
u8 gunnerPattern;
bool dronesShoot;
#define LEVEL_COUNT 15
#define TP_POOL_SIZE 5
// pool index -> enemy type mapping
static const u8 poolTypeMap[TP_POOL_SIZE] = {
ENEMY_TYPE_TEST, // 0: Fairy
ENEMY_TYPE_DRONE, // 1: Drone
ENEMY_TYPE_GUNNER, // 2: Gunner
ENEMY_TYPE_HUNTER, // 3: Hunter
ENEMY_TYPE_BUILDER, // 4: Builder
};
// dr gn hn bl boss tre pat shoot
const struct LevelDef levels[20] = {
// Phase 1: "Immediate danger" (L1-L4)
{ 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1
{ 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2
{ 12, 3, 0, 0, 0, 8, 1, TRUE }, // L3
{ 8, 0, 0, 0, 25, 8, 0, TRUE }, // L4 BOSS 1
// TP costs per pool index
static const u8 typeCost[TP_POOL_SIZE] = { 5, 2, 4, 3, 3 };
static const u8 typeWeight[TP_POOL_SIZE] = { 2, 8, 4, 3, 3 };
static const u8 typeMaxCount[TP_POOL_SIZE] = { 3, 16, 6, 6, 2 };
static const u8 typeMinCount[TP_POOL_SIZE] = { 0, 2, 0, 0, 0 };
// Phase 2: "You can't save everything" (L5-L8)
{ 10, 2, 0, 1, 0, 8, 0, TRUE }, // L5
{ 14, 3, 0, 1, 0, 8, 1, TRUE }, // L6
{ 10, 2, 0, 2, 0, 8, 2, TRUE }, // L7
{ 8, 0, 0, 1, 50, 8, 0, TRUE }, // L8 BOSS 2
// Boss HP per boss number (0-4)
static const s16 bossHpTable[5] = { 24, 50, 75, 100, 125 };
// Phase 3: "Geometry matters" (L9-L12)
{ 8, 3, 4, 0, 0, 8, 1, TRUE }, // L9
{ 10, 2, 4, 0, 0, 8, 2, TRUE }, // L10
{ 12, 3, 3, 0, 0, 8, 1, TRUE }, // L11
{ 0, 2, 2, 0, 75, 8, 2, TRUE }, // L12 BOSS 3
// Returns bitmask of unlocked pool indices for a given level
static u8 getUnlockedTypes(u8 lvl){
u8 mask = 0;
// Drone always unlocked
mask |= (1 << 1);
// Gunner from L2 (index 1)
if(lvl >= 1) mask |= (1 << 2);
// Fairy + Builder from L4 (index 3)
if(lvl >= 3){
mask |= (1 << 0);
mask |= (1 << 4);
}
// Hunter from L7 (index 6)
if(lvl >= 6) mask |= (1 << 3);
return mask;
}
// Phase 4: "Suffocation" (L13-L16)
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L13
{ 10, 0, 6, 0, 0, 8, 0, TRUE }, // L14
{ 12, 4, 2, 0, 0, 8, 1, TRUE }, // L15
{ 0, 3, 0, 1, 100, 8, 2, TRUE }, // L16 BOSS 4
static u8 getTreasureCount(u8 lvl){
if(lvl == 0) return 4;
if(lvl <= 2) return 6;
return 8;
}
// Phase 5: "Arcade cruelty" (L17-L20)
{ 16, 0, 4, 0, 0, 8, 0, TRUE }, // L17
{ 14, 4, 4, 2, 0, 8, 2, TRUE }, // L18
{ 6, 2, 2, 1, 50, 8, 2, TRUE }, // L19 MINI-BOSS
{ 4, 2, 2, 1, 125, 8, 2, TRUE }, // L20 BOSS 5 FINAL
};
static void assignGunnerPatterns(u8 lvl){
u8 pat;
if(lvl < 3) pat = 0; // Cycle 1: radial burst
else if(lvl < 6) pat = 1; // Cycle 2: aimed fan
else pat = 2; // Cycle 3+: mix
#define LEVEL_COUNT 20
for(s16 i = 0; i < ENEMY_COUNT; i++){
if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){
if(pat == 2)
enemies[i].ints[0] = random() % 2;
else
enemies[i].ints[0] = pat;
}
}
}
static void distributeEnemies(u8 type, u8 count){
for(u8 i = 0; i < count; i++){
u8 zone = i % 4;
u8 zone = i % SECTION_COUNT;
spawnEnemy(type, zone);
}
}
// Generate enemy counts into the provided array (indexed by pool index)
static void generateLevel(u8 lvl, u8* counts){
for(u8 i = 0; i < TP_POOL_SIZE; i++) counts[i] = 0;
// L1 special case: 4 non-shooting drones
if(lvl == 0){
counts[1] = 4;
return;
}
// Compute budget
u16 budget = 8 + (lvl * 4) + (lvl * lvl / 5);
budget = (budget * (90 + (random() % 21))) / 100;
// Boss levels get 40% escort budget
if(isBossLevel(lvl)){
budget = (budget * 40) / 100;
if(budget < 4) budget = 4;
}
u8 unlocked = getUnlockedTypes(lvl);
u8 maxTotal = isBossLevel(lvl) ? ENEMY_COUNT - 1 : ENEMY_COUNT; // reserve 1 slot for boss
// Apply minimum guarantees
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if((unlocked & (1 << i)) && typeMinCount[i] > 0){
counts[i] = typeMinCount[i];
u16 cost = typeMinCount[i] * typeCost[i];
if(cost <= budget) budget -= cost;
else budget = 0;
}
}
// Shopping loop
u16 safety = 0;
u8 totalEnemies = 0;
for(u8 i = 0; i < TP_POOL_SIZE; i++) totalEnemies += counts[i];
while(budget >= 2 && totalEnemies < maxTotal && safety < 100){
safety++;
// Boss escort restriction: only drones + builders
u8 escortMask = isBossLevel(lvl) ? ((1 << 1) | (1 << 4)) : 0xFF;
// Build weighted pool of affordable, unlocked, non-maxed types
u16 totalWeight = 0;
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if(!(unlocked & (1 << i))) continue;
if(!(escortMask & (1 << i))) continue;
if(counts[i] >= typeMaxCount[i]) continue;
if(typeCost[i] > budget) continue;
totalWeight += typeWeight[i];
}
if(totalWeight == 0) break;
// Weighted random pick
u16 roll = random() % totalWeight;
u16 accum = 0;
u8 picked = 0xFF;
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if(!(unlocked & (1 << i))) continue;
if(!(escortMask & (1 << i))) continue;
if(counts[i] >= typeMaxCount[i]) continue;
if(typeCost[i] > budget) continue;
accum += typeWeight[i];
if(roll < accum){
picked = i;
break;
}
}
if(picked == 0xFF) break;
counts[picked]++;
budget -= typeCost[picked];
totalEnemies++;
}
}
void loadLevel(u8 lvl){
if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1;
level = lvl;
const struct LevelDef* def = &levels[lvl];
grazeCount = 0;
distributeEnemies(ENEMY_TYPE_DRONE, def->drones);
distributeEnemies(ENEMY_TYPE_GUNNER, def->gunners);
distributeEnemies(ENEMY_TYPE_HUNTER, def->hunters);
distributeEnemies(ENEMY_TYPE_BUILDER, def->builders);
// Generate enemy composition
u8 counts[TP_POOL_SIZE];
generateLevel(lvl, counts);
// set gunner pattern based on level def
for(s16 i = 0; i < ENEMY_COUNT; i++){
if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){
if(def->gunnerPattern == 2)
enemies[i].ints[0] = random() % 2;
else
enemies[i].ints[0] = def->gunnerPattern;
}
// Spawn enemies by type
for(u8 i = 0; i < TP_POOL_SIZE; i++){
if(counts[i] > 0)
distributeEnemies(poolTypeMap[i], counts[i]);
}
if(def->bossHp > 0){
pendingBossHp = def->bossHp;
pendingBossNum = lvl / 4; // L3=0, L7=1, L11=2, L15=3, L18+=4
// Assign gunner patterns
assignGunnerPatterns(lvl);
// Boss spawn
if(isBossLevel(lvl)){
pendingBossNum = lvl / 3;
if(pendingBossNum > 4) pendingBossNum = 4;
if(lvl == 18) pendingBossNum = 1; // L19 mini-boss reuses boss 2
pendingBossHp = bossHpTable[pendingBossNum];
spawnEnemy(ENEMY_TYPE_BOSS, 1);
}
// spawn treasures
u8 treasureToSpawn = def->treasures;
for(u8 zone = 0; zone < 4 && treasureToSpawn > 0; zone++){
// Spawn treasures
u8 treasureToSpawn = getTreasureCount(lvl);
for(u8 zone = 0; zone < SECTION_COUNT && treasureToSpawn > 0; zone++){
u8 perZone = treasureToSpawn >= 4 ? 2 : 1;
for(u8 h = 0; h < perZone && treasureToSpawn > 0; h++){
spawnTreasure(zone);
@ -178,6 +193,7 @@ void loadLevel(u8 lvl){
}
}
loadBgPalette(lvl % 3);
loadMap();
}

107
src/starfield.h Normal file
View file

@ -0,0 +1,107 @@
#define STAR_TILE_I 290 // tile 290 = faded, 291 = full bright
#define STAR_COUNT 64
#define STAR_SPEED FIX16(0.7) // base max speed in tiles/frame (try 0.31.5)
#define STAR_FADE_FRAMES 5 // frames: tile 290 (faded)
#define STAR_MID_FRAMES 20 // frames: tile 291 (mid)
#define STAR_BRIGHT_FRAMES 40 // frames: tile 292 (bright); tile 293 (full) after this
#define STAR_SCREEN_W 40
#define STAR_SCREEN_H 28
#define STAR_CENTER_X 20
#define STAR_CENTER_Y 14
typedef struct {
fix16 x, y;
fix16 dx, dy;
u8 prevX, prevY;
u8 age; // frames since spawn; drives tile stage
u8 variant; // 0 = base tiles (290-293), 1 = alt color tiles (294-297)
} StarParticle;
static StarParticle stars[STAR_COUNT];
static u8 spawnCounter;
static u8 starVariant;
static void spawnStar(u8 i){
fix16 angleDeg = FIX16(random() % 360);
fix16 speed = F16_mul(STAR_SPEED, FIX16(0.25) + (fix16)((random() % 8) * FIX16(0.11)));
stars[i].x = FIX16(STAR_CENTER_X);
stars[i].y = FIX16(STAR_CENTER_Y);
stars[i].dx = F16_mul(F16_cos(angleDeg), speed);
stars[i].dy = F16_mul(F16_sin(angleDeg), speed);
stars[i].prevX = 255;
stars[i].prevY = 255;
stars[i].age = 0;
stars[i].variant = starVariant;
spawnCounter++;
}
// Advance a star forward by a random number of frames (no VDP writes).
// If it exits the screen during pre-advance, respawn it from center.
static void preadvanceStar(u8 i){
u8 frames = (u8)(random() % 90);
for(u8 f = 0; f < frames; f++){
stars[i].x += stars[i].dx;
stars[i].y += stars[i].dy;
if(stars[i].age < STAR_BRIGHT_FRAMES) stars[i].age++;
s16 tx = F16_toInt(stars[i].x);
s16 ty = F16_toInt(stars[i].y);
if(tx < 0 || tx >= STAR_SCREEN_W || ty < 0 || ty >= STAR_SCREEN_H){
spawnStar(i);
return;
}
}
}
void loadStarfield(u8 variant){
starVariant = variant;
VDP_loadTileSet(&starTiles, STAR_TILE_I, DMA);
// Reset BG_B scroll so stars appear at screen positions 0-39, 0-27
VDP_setVerticalScroll(BG_B, 0);
s16 zeroScroll[28] = {0};
VDP_setHorizontalScrollTile(BG_B, 0, zeroScroll, 28, DMA);
spawnCounter = 0;
for(u8 i = 0; i < STAR_COUNT; i++){
spawnStar(i);
preadvanceStar(i);
}
}
void clearStarfield(){
// Erase any star tiles still on BG_B before the next level loads
for(u8 i = 0; i < STAR_COUNT; i++){
if(stars[i].prevX != 255){
VDP_setTileMapXY(BG_B, TILE_ATTR(0, 0, 0, 0), stars[i].prevX, stars[i].prevY);
stars[i].prevX = 255;
stars[i].prevY = 255;
}
}
}
void updateStarfield(){
for(u8 i = 0; i < STAR_COUNT; i++){
if(stars[i].prevX != 255)
VDP_setTileMapXY(BG_B, TILE_ATTR(0, 0, 0, 0), stars[i].prevX, stars[i].prevY);
stars[i].x += stars[i].dx;
stars[i].y += stars[i].dy;
if(stars[i].age < STAR_BRIGHT_FRAMES) stars[i].age++;
s16 tx = F16_toInt(stars[i].x);
s16 ty = F16_toInt(stars[i].y);
if(tx < 0 || tx >= STAR_SCREEN_W || ty < 0 || ty >= STAR_SCREEN_H){
spawnStar(i);
continue;
}
u16 base = STAR_TILE_I + stars[i].variant * 4;
u16 tileIdx = (stars[i].age < STAR_FADE_FRAMES) ? base :
(stars[i].age < STAR_MID_FRAMES) ? (base + 1) :
(stars[i].age < STAR_BRIGHT_FRAMES) ? (base + 2) : (base + 3);
VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, tileIdx), (u16)tx, (u16)ty);
stars[i].prevX = (u8)tx;
stars[i].prevY = (u8)ty;
}
}

View file

@ -5,40 +5,75 @@
s16 startClock;
static void updateTransition(s16 startTime, bool last){
if(startClock >= startTime && startClock < startTime + TRANS_TIME){
switch(startClock - startTime){
case 0: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break;
case 5: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break;
case 10: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break;
case 15: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H); break;
case 20: VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H); break;
case TRANS_TIME - 20: if(!last){
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H);
} break;
case TRANS_TIME - 15: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break;
case TRANS_TIME - 10: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break;
case TRANS_TIME - 5: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break;
}
}
static void drawStartSplash(){
VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 12, 6, 0, DMA);
}
static void drawStartSplash(){
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I), 0, 0, START_W, START_H);
VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 13, 7, 0, DMA);
}
s16 startScroll;
static void drawStartBg(){
VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H);
VDP_drawImageEx(BG_B, &startBigBg, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16), 0, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg1, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16), 0, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg2, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196), 14, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg3, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196), 14 + 14, 0, 0, DMA);
VDP_drawImageEx(BG_B, &startBg4, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196 + 168), 0, 14, 0, DMA);
VDP_drawImageEx(BG_B, &startBg5, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196 + 168 + 196), 14, 14, 0, DMA);
VDP_drawImageEx(BG_B, &startBg6, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16 + 196 + 196 + 168 + 196 + 196), 14 + 14, 14, 0, DMA);
startScroll = -160;
VDP_setVerticalScroll(BG_A, startScroll);
}
// State variables
static s16 startState; // 0 = main menu, 1 = music room
static s16 menuCursor; // 0 = START GAME, 1 = MUSIC ROOM
static s16 musicCursor; // 0-2, highlighted track
static s16 musicPlaying; // -1 = none, 0-2 = playing track index
static s16 musicDelay; // frames to delay before starting new track
static bool playBgmStart; // TRUE to play bgmStart after delay (from exiting music room)
static bool attractPending; // TRUE once pre-attract fade-out starts (blocks input)
static const char* trackNames[3] = {
"Title",
"Stage",
"Boss "
};
static const u8* musicTrackList[3];
#define MENU_X 14
#define MENU_Y 16
static void drawStartMenuCursors(){
VDP_drawText(menuCursor == 0 ? "%Start Game" : " Start Game", MENU_X, MENU_Y);
VDP_drawText(menuCursor == 1 ? "%Music Room" : " Music Room", MENU_X, MENU_Y + 2);
}
static bool startMenuTopDrawn, startMenuMidDrawn, startMenuBottomDrawn;
static void drawStartMenuTop(){
VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, 1111), 7, 7, 0, DMA);
VDP_drawText(" DRAGON-EATING DESCENT II", 7, 12);
startMenuTopDrawn = TRUE;
}
static void drawStartMenuMid(){
drawStartMenuCursors();
startMenuMidDrawn = TRUE;
}
static void drawStartMenuBottom(){
char hiScoreStr[SCORE_LENGTH];
uintToStr(highScore, hiScoreStr, 1);
VDP_drawText("High", 2, 25);
VDP_drawText(hiScoreStr, 2 + 5, 25);
VDP_drawText("2026 Peace Research", 38 - 19, 25);
startMenuBottomDrawn = TRUE;
}
static void drawStartMenu(){
// VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 16 + 256), 26, 2, 0, DMA);
VDP_drawText(" PRESS ANY", 19, 18);
VDP_drawText(" BUTTON", 19, 19);
VDP_drawText(" T. BODDY", 19, 24);
VDP_drawText(" 02.2026", 19, 25);
drawStartMenuTop();
drawStartMenuMid();
drawStartMenuBottom();
}
static void loadGameFromStart(){
@ -49,12 +84,122 @@ static void loadGameFromStart(){
loadGame();
}
s16 startTime;
static void updateStartMenu(){
if(startTime == 0 && (ctrl.start || ctrl.a || ctrl.b || ctrl.c)){
XGM2_stop();
startTime = 30;
static void loadAttractFromStart(){
isAttract = TRUE;
attractClock = ATTRACT_DURATION;
loadGameFromStart();
}
#define MUSIC_NP_Y 25
#define MUSIC_CTRL_Y 17
static void drawMusicRoomCursors(){
for(s16 i = 0; i < 3; i++){
char line[16];
line[0] = (musicCursor == i) ? '%' : ' ';
line[1] = ' ';
const char* name = trackNames[i];
for(s16 j = 0; j < 11; j++) line[1 + j] = name[j];
line[13] = '\0';
VDP_drawText(line, 16, 13 + i);
}
}
static void drawMusicRoomNowPlaying(){
if(musicPlaying >= 0){
char line[32];
const char* prefix = " Now Playing: ";
for(s16 i = 0; i < 15; i++) line[i] = prefix[i];
const char* name = trackNames[musicPlaying];
for(s16 i = 0; i < 11; i++) line[15 + i] = name[i];
line[26] = '\0';
VDP_drawText(line, 9, MUSIC_NP_Y);
} else {
VDP_drawText(" ", 9, MUSIC_NP_Y);
}
}
static void drawMusicRoom(){
VDP_drawImageEx(BG_A, &musicroom, TILE_ATTR_FULL(PAL0, 0, 0, 0, 1111), 15, 8, 0, DMA);
drawMusicRoomCursors();
VDP_drawText("[ Play/Stop ", 14, MUSIC_CTRL_Y);
VDP_drawText("] Back ", 14, MUSIC_CTRL_Y + 1);
drawMusicRoomNowPlaying();
}
static void enterMusicRoom(){
XGM2_stop();
startState = 1;
musicCursor = 0;
musicPlaying = -1;
VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H);
drawMusicRoom();
}
static void exitMusicRoom(){
XGM2_stop();
musicPlaying = -1;
musicDelay = 1;
playBgmStart = TRUE;
startState = 0;
startClock = TRANS_TIME + 41;
VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H);
drawStartMenu();
}
s16 startTime;
static bool prevUp, prevDown, prevA, prevB, prevC, prevStart;
static void updateStartMainMenu(){
if(attractPending) return;
// handle music delay
if(musicDelay > 0){
musicDelay--;
if(musicDelay == 0 && playBgmStart){
#if MUSIC_VOLUME > 0
XGM2_play(bgmStart);
#endif
playBgmStart = FALSE;
}
}
// Up/down cursor movement
bool curUp = ctrl.up;
bool curDown = ctrl.down;
if(curUp && !prevUp){
menuCursor = (menuCursor == 0) ? 1 : 0;
drawStartMenuCursors();
sfxMenuSelect();
startClock = TRANS_TIME + 21;
}
if(curDown && !prevDown){
menuCursor = (menuCursor == 0) ? 1 : 0;
drawStartMenuCursors();
sfxMenuSelect();
startClock = TRANS_TIME + 21;
}
prevUp = curUp;
prevDown = curDown;
// Confirm selection
bool curA = ctrl.a;
bool curB = ctrl.b;
bool curSt = ctrl.start;
if((curA && !prevA) || (curSt && !prevStart)){
if(startTime == 0){
if(menuCursor == 0){
startTime = 30;
sfxStartGame();
PAL_fadeOut(0, 47, 20, TRUE);
} else {
enterMusicRoom();
}
}
}
prevA = curA;
prevB = curB;
prevStart = curSt;
if(startTime > 0){
startTime--;
if(startTime <= 0){
@ -63,20 +208,110 @@ static void updateStartMenu(){
}
}
static void updateMusicRoom(){
// handle music delay
if(musicDelay > 0){
musicDelay--;
if(musicDelay == 0 && musicPlaying >= 0){
#if MUSIC_VOLUME > 0
XGM2_play(musicTrackList[musicPlaying]);
#endif
}
}
bool curUp = ctrl.up;
bool curDown = ctrl.down;
bool curA = ctrl.a;
bool curB = ctrl.b;
bool curSt = ctrl.start;
if(curUp && !prevUp){
musicCursor = (musicCursor == 0) ? 2 : musicCursor - 1;
drawMusicRoomCursors();
sfxMenuSelect();
}
if(curDown && !prevDown){
musicCursor = (musicCursor == 2) ? 0 : musicCursor + 1;
drawMusicRoomCursors();
sfxMenuSelect();
}
if((curA && !prevA) || (curSt && !prevStart)){
if(musicPlaying == musicCursor){
XGM2_stop();
musicPlaying = -1;
} else {
XGM2_stop();
musicDelay = 1;
musicPlaying = musicCursor;
}
drawMusicRoomNowPlaying();
}
if(curB && !prevB){
exitMusicRoom();
}
prevUp = curUp;
prevDown = curDown;
prevA = curA;
prevB = curB;
prevStart = curSt;
}
void loadStart(){
VDP_loadTileSet(startFade1.tileset, START_I, DMA);
VDP_loadTileSet(startFade2.tileset, START_I + 1, DMA);
VDP_loadTileSet(startFade3.tileset, START_I + 2, DMA);
VDP_loadTileSet(startFade4.tileset, START_I + 3, DMA);
static const u16 palBlack[64] = {0};
PAL_setColors(0, palBlack, 64, CPU);
getHighScore();
musicTrackList[0] = bgmStart;
musicTrackList[1] = stageMusic;
musicTrackList[2] = bossMusic;
startState = 0;
menuCursor = 0;
musicCursor = 0;
musicPlaying = -1;
prevUp = FALSE; prevDown = FALSE; prevA = FALSE;
prevB = FALSE; prevC = FALSE; prevStart = FALSE;
attractPending = FALSE;
startMenuTopDrawn = FALSE;
startMenuMidDrawn = FALSE;
startMenuBottomDrawn = FALSE;
drawStartSplash();
XGM2_play(bgmStart);
}
void updateStart(){
updateTransition(0, FALSE);
updateTransition(TRANS_TIME, TRUE);
if(startClock == TRANS_TIME) drawStartBg();
else if(startClock == TRANS_TIME + 40) drawStartMenu();
else if(startClock > TRANS_TIME + 40) updateStartMenu();
if(startScroll < 0 && startClock >= TRANS_TIME){
startScroll += 8;
// if(startScroll > 0) startScroll = 0;
VDP_setVerticalScroll(BG_A, startScroll);
}
if(startClock == 0)
PAL_fadeIn(0, 15, font.palette->data, 20, TRUE);
if(startClock == TRANS_TIME - 20)
PAL_fadeOutAll(20, TRUE);
if(startClock == TRANS_TIME){
drawStartBg();
#if MUSIC_VOLUME > 0
XGM2_play(bgmStart);
#endif
u16 menuPal[32];
memcpy(menuPal, font.palette->data, 16 * sizeof(u16));
memcpy(menuPal + 16, shadow.palette->data, 16 * sizeof(u16));
PAL_fadeIn(0, 31, menuPal, 20, TRUE);
drawStartMenuTop();
}
// stagger menu/bottom text to avoid plane-wrap visibility
if(startClock > TRANS_TIME){
if(!startMenuMidDrawn && startScroll >= -104) drawStartMenuMid();
if(!startMenuBottomDrawn && startScroll >= -32) drawStartMenuBottom();
}
if(startClock > TRANS_TIME + 20){
if(startState == 0) updateStartMainMenu();
else updateMusicRoom();
}
// Attract mode only from main menu
if(startState == 0 && !attractPending && startClock == TRANS_TIME + 20 + ATTRACT_LIMIT - 20 && startTime == 0){
attractPending = TRUE;
PAL_fadeOut(0, 31, 20, TRUE);
}
if(startState == 0 && startClock >= TRANS_TIME + 20 + ATTRACT_LIMIT && startTime == 0) loadAttractFromStart();
if(startClock < CLOCK_LIMIT) startClock++;
}

View file

@ -19,12 +19,13 @@ void spawnTreasure(u8 zone){
treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
treasures[i].image = SPR_addSprite(&treasureSprite,
getScreenX(treasures[i].pos.x, player.camera) - TREASURE_OFF, fix32ToInt(treasures[i].pos.y) - TREASURE_OFF,
getScreenX(treasures[i].pos.x, player.camera) - TREASURE_OFF, F32_toInt(treasures[i].pos.y) - TREASURE_OFF,
TILE_ATTR(PAL0, 0, 0, 0));
if(!treasures[i].image){
treasures[i].active = FALSE;
return;
}
SPR_setDepth(treasures[i].image, 2);
SPR_setVisibility(treasures[i].image, HIDDEN);
treasures[i].type = random() % 4;
SPR_setAnim(treasures[i].image, treasures[i].type);
@ -34,8 +35,13 @@ static void updateTreasure(u8 i){
switch(treasures[i].state){
case TREASURE_WALKING:
// Y bounce: bob 4px around ground level
if(treasures[i].pos.y >= GAME_H_F - FIX32(20) || treasures[i].pos.y <= GAME_H_F - FIX32(28))
treasures[i].vel.y *= -1;
if(treasures[i].pos.y >= GAME_H_F - FIX32(16)){
if(treasures[i].vel.y > 0) treasures[i].vel.y = -treasures[i].vel.y;
treasures[i].pos.y = GAME_H_F - FIX32(16);
} else if(treasures[i].pos.y <= GAME_H_F - FIX32(32)){
if(treasures[i].vel.y < 0) treasures[i].vel.y = -treasures[i].vel.y;
treasures[i].pos.y = GAME_H_F - FIX32(32);
}
// X wrap
if(treasures[i].pos.x >= GAME_WRAP)
@ -43,8 +49,8 @@ static void updateTreasure(u8 i){
if(treasures[i].pos.x < 0)
treasures[i].pos.x += GAME_WRAP;
treasures[i].pos.x += treasures[i].vel.x;
treasures[i].pos.y += treasures[i].vel.y;
treasures[i].pos.x += treasures[i].vel.x - (player.vel.x >> 2);
treasures[i].pos.y += treasures[i].vel.y - (playerScrollVelY >> 3);
break;
case TREASURE_CARRIED:
@ -109,8 +115,20 @@ static void updateTreasure(u8 i){
if(treasures[i].state != TREASURE_CARRIED && treasures[i].state != TREASURE_COLLECTED){
fix32 dy = treasures[i].pos.y - player.pos.y;
if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){
score += (treasures[i].state == TREASURE_FALLING) ? 2000 : 1000;
sfxPickup();
score += (treasures[i].state == TREASURE_FALLING) ? 4096 : 1024;
// check if this is the last treasure (all others inactive or collected)
bool willBeLast = TRUE;
for(s16 j = 0; j < TREASURE_COUNT; j++){
if(j != i && treasures[j].active && treasures[j].state != TREASURE_COLLECTED){
willBeLast = FALSE;
break;
}
}
if(willBeLast){
sfxCollectAllTreasures();
} else {
sfxCollectTreasure();
}
treasureCollectedType = treasures[i].type;
treasureCollectedClock = 120;
// only add to trail if this type isn't already collected
@ -133,12 +151,12 @@ static void updateTreasure(u8 i){
}
s16 sx = getScreenX(treasures[i].pos.x, player.camera);
s16 sy = fix32ToInt(treasures[i].pos.y);
s16 sy = F32_toInt(treasures[i].pos.y);
bool visible = (treasures[i].state == TREASURE_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
if(visible && treasures[i].state == TREASURE_COLLECTED){
if(player.respawnClock > 0)
visible = FALSE;
else if(player.recoveringClock > 0)
else if(player.recoveringClock > 0 && player.recoverFlash)
visible = (player.recoveringClock % 20 > 10);
}
SPR_setVisibility(treasures[i].image, visible ? VISIBLE : HIDDEN);