shit
5
build.sh
|
|
@ -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"
|
||||
|
|
@ -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
BIN
res/bullets.png
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
BIN
res/door.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
res/enemies/boss1.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
res/enemies/boss2.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
res/enemies/boss3.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
res/enemies/boss4.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
res/eyebig.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
res/font.png
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
res/fontbig.png
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
res/fontbigger.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
res/ground.png
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
res/life.png
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
res/life2.png
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2 KiB |
BIN
res/musicroom.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -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
BIN
res/sfx/beatlevel.wav
Normal file
BIN
res/sfx/bullet1.wav
Normal file
BIN
res/sfx/bullet2.wav
Normal file
BIN
res/sfx/bullet3.wav
Normal file
BIN
res/sfx/explosion1.wav
Normal file
BIN
res/sfx/explosion2.wav
Normal file
BIN
res/sfx/gameover.wav
Normal file
BIN
res/sfx/menuchoose.wav
Normal file
BIN
res/sfx/menuselect.wav
Normal file
BIN
res/sfx/playerhit.wav
Normal file
BIN
res/sfx/playershot.wav
Normal file
BIN
res/sfx/startgame.wav
Normal file
BIN
res/sky.png
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.2 KiB |
BIN
res/skyred.png
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
res/skytop.png
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5 KiB |
BIN
res/stars.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
res/start/bg1.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
res/start/bg2.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
res/start/bg3.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
res/start/bg4.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
res/start/bg5.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
res/start/bg6.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 8.4 KiB |
3
run.sh
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
120
src/boot/sega.s
|
|
@ -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
|
||||
|
|
|
|||
157
src/bullets.h
|
|
@ -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);
|
||||
|
|
|
|||
234
src/chrome.h
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
1116
src/enemytypes.h
133
src/global.h
|
|
@ -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);
|
||||
}
|
||||
81
src/main.c
|
|
@ -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();
|
||||
|
|
|
|||
101
src/player.h
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
298
src/stage.h
|
|
@ -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
|
|
@ -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.3–1.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;
|
||||
}
|
||||
}
|
||||
317
src/start.h
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||