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

@ -2,5 +2,6 @@ rm -rf res/resources.o res/resources.h out/*
# make
# ./blastem/blastem out.bin
#dgen out.bin
docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00
/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin
docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.11
# /Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin
/Applications/ares.app/Contents/MacOS/ares out/rom.bin --system "Mega Drive"

View file

@ -1,2 +1,2 @@
rm -rf res/resources.o res/resources.h out/*
docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00
docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.11

BIN
default.profraw Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

BIN
res/door.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
res/enemies/boss1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
res/enemies/boss2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
res/enemies/boss3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
res/enemies/boss4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
res/eyebig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

BIN
res/fontbigger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

BIN
res/musicroom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,19 +1,22 @@
IMAGE font "font.png" NONE NONE
IMAGE shadow "shadow.png" NONE NONE
IMAGE startFade1 "start/fade1.png" FAST
IMAGE startFade2 "start/fade2.png" FAST
IMAGE startFade3 "start/fade3.png" FAST
IMAGE startFade4 "start/fade4.png" FAST
IMAGE startSplash1 "start/splash1.png" FAST
IMAGE startLogo "start/logo.png" FAST
IMAGE startBigBg "start/bigbg.png" FAST
IMAGE musicroom "musicroom.png" FAST
IMAGE startBg1 "start/bg1.png" FAST
IMAGE startBg2 "start/bg2.png" FAST
IMAGE startBg3 "start/bg3.png" FAST
IMAGE startBg4 "start/bg4.png" FAST
IMAGE startBg5 "start/bg5.png" FAST
IMAGE startBg6 "start/bg6.png" FAST
XGM2 bgmStart "start.vgm"
IMAGE sky "sky.png" NONE NONE
IMAGE skyTop "skytop.png" NONE NONE
IMAGE skyRed "skyred.png" NONE NONE
IMAGE ground "ground.png" NONE NONE
IMAGE door "door.png" NONE NONE
SPRITE momoyoSprite "momoyo.png" 6 6 NONE 0
@ -21,13 +24,39 @@ SPRITE bulletsSprite "bullets.png" 2 2 NONE 0
SPRITE pBulletSprite "pbullet.png" 4 4 NONE 0
SPRITE fairySprite "fairy2.png" 4 4 NONE 8
SPRITE eyeBigSprite "eyebig.png" 4 4 NONE 0
SPRITE boss1Sprite "enemies/boss1.png" 6 6 NONE 0
SPRITE boss2Sprite "enemies/boss2.png" 6 6 NONE 0
SPRITE boss3Sprite "enemies/boss3.png" 6 6 NONE 0
SPRITE boss4Sprite "enemies/boss4.png" 6 6 NONE 0
SPRITE treasureSprite "treasure.png" 4 4 NONE 0
IMAGE mapIndicator "mapindicator.png" NONE NONE
TILESET starTiles "stars.png" NONE
IMAGE imageFontBig "fontbig.png" NONE NONE
IMAGE imageFontBigger "fontbigger.png" NONE NONE
IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE
IMAGE imageChromeLife "life.png" NONE NONE
IMAGE imageChromeLife2 "life2.png" NONE NONE
XGM2 stageMusic "level.vgm"
XGM2 bossMusic "boss.vgm"
XGM2 treasureMusic "treasure.vgm"
WAV sfxSamplePlayerShot "sfx/playershot.wav" XGM2
WAV sfxSampleBullet1 "sfx/bullet1.wav" XGM2
WAV sfxSampleBullet2 "sfx/bullet2.wav" XGM2
WAV sfxSampleBullet3 "sfx/bullet3.wav" XGM2
WAV sfxSampleExplosion "sfx/explosion1.wav" XGM2
WAV sfxSamplePickup "sfx/menuchoose.wav" XGM2
WAV sfxSampleGraze "sfx/menuselect.wav" XGM2
WAV sfxSampleStartGame "sfx/menuchoose.wav" XGM2
WAV sfxSamplePlayerHit "sfx/playerhit.wav" XGM2
WAV sfxSampleMenuChoose "sfx/menuchoose.wav" XGM2
WAV sfxSampleMenuSelect "sfx/menuselect.wav" XGM2
WAV sfxSampleGameOver "sfx/gameover.wav" XGM2
WAV sfxSampleCollectTreasure "sfx/beatlevel.wav" XGM2
WAV sfxSampleCollectAllTreasures "sfx/startgame.wav" XGM2

BIN
res/sfx/beatgame.wav Normal file

Binary file not shown.

BIN
res/sfx/beatlevel.wav Normal file

Binary file not shown.

BIN
res/sfx/bullet1.wav Normal file

Binary file not shown.

BIN
res/sfx/bullet2.wav Normal file

Binary file not shown.

BIN
res/sfx/bullet3.wav Normal file

Binary file not shown.

BIN
res/sfx/explosion1.wav Normal file

Binary file not shown.

BIN
res/sfx/explosion2.wav Normal file

Binary file not shown.

BIN
res/sfx/gameover.wav Normal file

Binary file not shown.

BIN
res/sfx/menuchoose.wav Normal file

Binary file not shown.

BIN
res/sfx/menuselect.wav Normal file

Binary file not shown.

BIN
res/sfx/playerhit.wav Normal file

Binary file not shown.

BIN
res/sfx/playershot.wav Normal file

Binary file not shown.

BIN
res/sfx/startgame.wav Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

BIN
res/stars.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
res/start/bg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
res/start/bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
res/start/bg3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
res/start/bg4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
res/start/bg5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
res/start/bg6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Before After
Before After

3
run.sh
View file

@ -1 +1,2 @@
/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin
#/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin
/Applications/ares.app/Contents/MacOS/ares out/rom.bin --system "Mega Drive"

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));
// 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));
// 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;
// }
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,21 +119,42 @@ 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);
// 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;
@ -159,6 +162,12 @@ static void collideWithPlayer(u8 i){
hitMessageBullet = TRUE;
}
}
} else if(dist <= GRAZE_RADIUS && !bullets[i].grazed){
bullets[i].grazed = TRUE;
score += 64;
grazeCount++;
sfxGraze();
}
}
static void updateBulletExplosion(u8 i){
@ -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(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 = fix32ToInt(enemies[i].pos.x) / 512;
u8 zone = F32_toInt(enemies[i].pos.x) / 512;
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
}
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,11 +207,14 @@ static void updateEnemy(u8 i){
enemies[i].hp = 0;
killEnemy(i);
}
if(!isAttract){
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
sfxGameOver();
} else {
sfxPlayerHit();
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
@ -193,10 +223,12 @@ static void updateEnemy(u8 i){
}
}
}
}
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);
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);

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){
if(bullets[i].player){
SPR_setAnim(bullets[i].image, 1);
} else {
s16 a = bullets[i].anim;
s16 explosionAnim;
if(a < FIRST_ROTATING_BULLET){
if(bullets[i].player){
explosionAnim = 16;
} else if(a < FIRST_ROTATING_BULLET){
explosionAnim = 13 + bullets[i].frame;
} else {
s16 mod = a % 3;
explosionAnim = 13 + mod;
}
SPR_setAnim(bullets[i].image, explosionAnim);
}
bullets[i].clock = 0;
bullets[i].frame = 0;
bullets[i].explosion = TRUE;
@ -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;
}
return arcAngle;
default: // FONT_THEME_RED
coloredPalette[i] = color;
break;
}
}
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,10 +6,11 @@
#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;
@ -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);