bonus stage
This commit is contained in:
parent
a8bc01bedd
commit
3263b2597b
11 changed files with 529 additions and 6 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
CLAUDE.md
|
*.md
|
||||||
.DS_Store
|
.DS_Store
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
|
||||||
BIN
res/bonus.png
Normal file
BIN
res/bonus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
res/bonusbg.png
Normal file
BIN
res/bonusbg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -31,6 +31,9 @@ SPRITE boss3Sprite "enemies/boss3.png" 6 6 NONE 0
|
||||||
SPRITE boss4Sprite "enemies/boss4.png" 6 6 NONE 0
|
SPRITE boss4Sprite "enemies/boss4.png" 6 6 NONE 0
|
||||||
SPRITE treasureSprite "treasure.png" 4 4 NONE 0
|
SPRITE treasureSprite "treasure.png" 4 4 NONE 0
|
||||||
|
|
||||||
|
SPRITE bonusObjSprite "bonus.png" 2 2 NONE 0
|
||||||
|
TILESET bonusBgTiles "bonusbg.png" NONE
|
||||||
|
|
||||||
IMAGE mapIndicator "mapindicator.png" NONE NONE
|
IMAGE mapIndicator "mapindicator.png" NONE NONE
|
||||||
TILESET starTiles "stars.png" NONE
|
TILESET starTiles "stars.png" NONE
|
||||||
|
|
||||||
|
|
|
||||||
BIN
res/stars.png
BIN
res/stars.png
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
469
src/bonus.h
Normal file
469
src/bonus.h
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
// Bonus Stage: minigame after boss fights
|
||||||
|
// Starfield background, sprites for objects. Stars = score, bombs = strikes.
|
||||||
|
// bonusObjSprite: anim 0 = star, anim 1 = bomb, frames 0-7 = depth (far to near)
|
||||||
|
|
||||||
|
#define BONUS_OBJ_COUNT 48
|
||||||
|
|
||||||
|
#define BONUS_VP_X 160 // vanishing point X (screen center)
|
||||||
|
#define BONUS_VP_Y 112 // vanishing point Y (screen center)
|
||||||
|
|
||||||
|
#define BONUS_RING_CX 160
|
||||||
|
#define BONUS_RING_CY 112
|
||||||
|
#define BONUS_RING_RX 120
|
||||||
|
#define BONUS_RING_RY 80
|
||||||
|
#define BONUS_ANGLE_SPEED FIX16(4) // max angular velocity
|
||||||
|
#define BONUS_ANGLE_ACCEL (BONUS_ANGLE_SPEED >> 3) // acceleration per frame
|
||||||
|
#define BONUS_ANGLE_FRICTION (BONUS_ANGLE_SPEED >> 3) // deceleration when no input
|
||||||
|
|
||||||
|
#define BONUS_COLLISION_DIST 24
|
||||||
|
#define BONUS_BOMB_COLLISION_DIST 14
|
||||||
|
#define BONUS_NEAR_SCALE FIX16(54) // collision window start
|
||||||
|
#define BONUS_KILL_SCALE FIX16(192) // fallback removal for objects stuck near center
|
||||||
|
|
||||||
|
// Depth frame thresholds (scale values, 8 stages like starfield brightness)
|
||||||
|
// Evenly spaced across 0 to BONUS_NEAR_SCALE
|
||||||
|
#define BONUS_DEPTH_COUNT 8
|
||||||
|
static const fix16 bonusDepthThresholds[BONUS_DEPTH_COUNT] = {
|
||||||
|
FIX16(0), FIX16(8), FIX16(16), FIX16(24), FIX16(32), FIX16(40), FIX16(48), FIX16(54)
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool active;
|
||||||
|
bool isBomb;
|
||||||
|
u8 depthFrame; // current depth frame (0-3)
|
||||||
|
fix16 scale;
|
||||||
|
fix16 scaleSpeed;
|
||||||
|
fix16 targetAngle;
|
||||||
|
fix16 angleVel;
|
||||||
|
s16 targetX, targetY;
|
||||||
|
Sprite* image;
|
||||||
|
} BonusObj;
|
||||||
|
|
||||||
|
static BonusObj bonusObjs[BONUS_OBJ_COUNT];
|
||||||
|
static Sprite* bonusCursor;
|
||||||
|
static fix16 bonusAngle;
|
||||||
|
static fix16 bonusAngleVel;
|
||||||
|
static s16 bonusCursorX, bonusCursorY;
|
||||||
|
|
||||||
|
static u32 bonusScoreVal;
|
||||||
|
static u16 bonusStarCount;
|
||||||
|
static u32 bonusCl;
|
||||||
|
bool bonusActive;
|
||||||
|
static bool bonusExiting;
|
||||||
|
static u32 bonusExitClock;
|
||||||
|
static bool bonusPaused;
|
||||||
|
static bool bonusPauseInput;
|
||||||
|
static u32 bonusPauseClock;
|
||||||
|
|
||||||
|
// --- Pattern system ---
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
s16 angleOffset; // degrees offset from pattern's base angle
|
||||||
|
u8 isBomb; // 0 = star, 1 = bomb
|
||||||
|
u8 delay; // frames to wait BEFORE spawning this entry (0 = same frame as previous)
|
||||||
|
} BonusPatternEntry;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const BonusPatternEntry* entries;
|
||||||
|
u8 count;
|
||||||
|
} BonusPattern;
|
||||||
|
|
||||||
|
// Pattern definitions
|
||||||
|
|
||||||
|
// Single star
|
||||||
|
static const BonusPatternEntry patSingleEntries[] = {
|
||||||
|
{0, 0, 0}
|
||||||
|
};
|
||||||
|
static const BonusPattern patSingle = {patSingleEntries, 1};
|
||||||
|
|
||||||
|
// 3 stars in a row, same spot
|
||||||
|
static const BonusPatternEntry patTripleEntries[] = {
|
||||||
|
{0, 0, 0}, {0, 0, 8}, {0, 0, 8}
|
||||||
|
};
|
||||||
|
static const BonusPattern patTriple = {patTripleEntries, 3};
|
||||||
|
|
||||||
|
// 5 stars in a row, same spot
|
||||||
|
static const BonusPatternEntry patStreamEntries[] = {
|
||||||
|
{0, 0, 0}, {0, 0, 8}, {0, 0, 8}, {0, 0, 8}, {0, 0, 8}
|
||||||
|
};
|
||||||
|
static const BonusPattern patStream = {patStreamEntries, 5};
|
||||||
|
|
||||||
|
// 3 stars then a bomb — must dodge after collecting
|
||||||
|
static const BonusPatternEntry patLineBombEntries[] = {
|
||||||
|
{0, 0, 0}, {0, 0, 8}, {0, 0, 8}, {0, 1, 8}
|
||||||
|
};
|
||||||
|
static const BonusPattern patLineBomb = {patLineBombEntries, 4};
|
||||||
|
|
||||||
|
// Bomb-star-bomb sandwich — must thread through
|
||||||
|
static const BonusPatternEntry patSandwichEntries[] = {
|
||||||
|
{0, 1, 0}, {0, 0, 8}, {0, 1, 8}
|
||||||
|
};
|
||||||
|
static const BonusPattern patSandwich = {patSandwichEntries, 3};
|
||||||
|
|
||||||
|
// Single bomb
|
||||||
|
static const BonusPatternEntry patBombEntries[] = {
|
||||||
|
{0, 1, 0}
|
||||||
|
};
|
||||||
|
static const BonusPattern patBomb = {patBombEntries, 1};
|
||||||
|
|
||||||
|
// Double bomb, same spot
|
||||||
|
static const BonusPatternEntry patBombPairEntries[] = {
|
||||||
|
{0, 1, 0}, {0, 1, 8}
|
||||||
|
};
|
||||||
|
static const BonusPattern patBombPair = {patBombPairEntries, 2};
|
||||||
|
|
||||||
|
// Tier pools (expanding by boss number)
|
||||||
|
static const BonusPattern* tierPool0[] = {&patSingle, &patTriple, &patBomb};
|
||||||
|
static const BonusPattern* tierPool1[] = {&patSingle, &patTriple, &patStream, &patBomb};
|
||||||
|
static const BonusPattern* tierPool2[] = {&patSingle, &patTriple, &patStream, &patLineBomb, &patBomb, &patBombPair};
|
||||||
|
static const BonusPattern* tierPool3[] = {&patSingle, &patTriple, &patStream, &patLineBomb, &patSandwich, &patBomb, &patBombPair};
|
||||||
|
static const BonusPattern* tierPool4[] = {&patSingle, &patTriple, &patStream, &patLineBomb, &patSandwich, &patBomb, &patBombPair};
|
||||||
|
|
||||||
|
static const BonusPattern** tierPools[] = {tierPool0, tierPool1, tierPool2, tierPool3, tierPool4};
|
||||||
|
static const u8 tierPoolCounts[] = {3, 4, 6, 7, 7};
|
||||||
|
static const u8 tierCooldowns[] = {24, 18, 14, 10, 8};
|
||||||
|
|
||||||
|
// Spiral: cycles through 0°/45°/90° sweep, alternating CW/CCW
|
||||||
|
static s8 bonusSpiralDir;
|
||||||
|
static u8 bonusSpiralPhase; // 0=straight, 1=45°, 2=90°
|
||||||
|
static const fix16 bonusSpiralMul[3] = {0, FIX16(0.556), FIX16(1.111)};
|
||||||
|
|
||||||
|
// Sequencer state
|
||||||
|
static const BonusPattern* bonusCurrentPattern;
|
||||||
|
static u8 bonusPatternIndex;
|
||||||
|
static u8 bonusPatternDelay;
|
||||||
|
static fix16 bonusPatternBase;
|
||||||
|
static u8 bonusPatternCooldown;
|
||||||
|
static u8 bonusBossNum;
|
||||||
|
|
||||||
|
#define BONUS_PAUSE_Y 14
|
||||||
|
|
||||||
|
static void drawBonusStars(){
|
||||||
|
char buf[6];
|
||||||
|
uintToStr(bonusStarCount, buf, 1);
|
||||||
|
bigText(buf, 1, 1, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Object spawning and update ---
|
||||||
|
|
||||||
|
static void spawnBonusObjAt(fix16 angle, bool isBomb){
|
||||||
|
s16 slot = -1;
|
||||||
|
for(u8 i = 0; i < BONUS_OBJ_COUNT; i++){
|
||||||
|
if(!bonusObjs[i].active){ slot = i; break; }
|
||||||
|
}
|
||||||
|
if(slot < 0) return;
|
||||||
|
|
||||||
|
BonusObj* obj = &bonusObjs[slot];
|
||||||
|
obj->isBomb = isBomb;
|
||||||
|
|
||||||
|
obj->targetAngle = F16_normalizeAngle(angle);
|
||||||
|
obj->targetX = BONUS_RING_CX + F16_toInt(F16_mul(F16_cos(obj->targetAngle), FIX16(BONUS_RING_RX)));
|
||||||
|
obj->targetY = BONUS_RING_CY + F16_toInt(F16_mul(F16_sin(obj->targetAngle), FIX16(BONUS_RING_RY)));
|
||||||
|
|
||||||
|
obj->scale = 0;
|
||||||
|
obj->scaleSpeed = FIX16(0.5);
|
||||||
|
if(bonusCl > 300) obj->scaleSpeed += FIX16(0.5);
|
||||||
|
if(bonusCl > 900) obj->scaleSpeed += FIX16(0.5);
|
||||||
|
|
||||||
|
// angleVel: sweep 0°/45°/90° over lifetime depending on phase
|
||||||
|
obj->angleVel = bonusSpiralDir * F16_mul(bonusSpiralMul[bonusSpiralPhase], obj->scaleSpeed);
|
||||||
|
|
||||||
|
obj->depthFrame = 0;
|
||||||
|
obj->image = SPR_addSprite(&bonusObjSprite, -16, -16, TILE_ATTR(PAL0, FALSE, FALSE, FALSE));
|
||||||
|
if(obj->image){
|
||||||
|
SPR_setAnim(obj->image, obj->isBomb ? 1 : 0);
|
||||||
|
SPR_setFrame(obj->image, 0);
|
||||||
|
obj->active = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateBonusObj(u8 i){
|
||||||
|
BonusObj* obj = &bonusObjs[i];
|
||||||
|
obj->scale += obj->scaleSpeed;
|
||||||
|
|
||||||
|
// Spiral: rotate target angle and recompute target position on ellipse
|
||||||
|
obj->targetAngle = F16_normalizeAngle(obj->targetAngle + obj->angleVel);
|
||||||
|
obj->targetX = BONUS_RING_CX + F16_toInt(F16_mul(F16_cos(obj->targetAngle), FIX16(BONUS_RING_RX)));
|
||||||
|
obj->targetY = BONUS_RING_CY + F16_toInt(F16_mul(F16_sin(obj->targetAngle), FIX16(BONUS_RING_RY)));
|
||||||
|
|
||||||
|
// Update depth frame based on scale thresholds (0-7)
|
||||||
|
u8 newFrame = 0;
|
||||||
|
for(u8 f = BONUS_DEPTH_COUNT - 1; f > 0; f--){
|
||||||
|
if(obj->scale >= bonusDepthThresholds[f]){ newFrame = f; break; }
|
||||||
|
}
|
||||||
|
if(newFrame != obj->depthFrame && obj->image){
|
||||||
|
obj->depthFrame = newFrame;
|
||||||
|
SPR_setFrame(obj->image, newFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate screen position from vanishing point toward target
|
||||||
|
fix16 diffX = (fix16)(obj->targetX - BONUS_VP_X);
|
||||||
|
fix16 diffY = (fix16)(obj->targetY - BONUS_VP_Y);
|
||||||
|
s16 sx = BONUS_VP_X + F16_toInt(F16_mul(diffX, obj->scale));
|
||||||
|
s16 sy = BONUS_VP_Y + F16_toInt(F16_mul(diffY, obj->scale));
|
||||||
|
|
||||||
|
// Collision check when near player depth
|
||||||
|
if(obj->scale >= BONUS_NEAR_SCALE && obj->scale < BONUS_KILL_SCALE){
|
||||||
|
s16 dx = sx - bonusCursorX;
|
||||||
|
s16 dy = sy - bonusCursorY;
|
||||||
|
s32 distSq = (s32)dx * dx + (s32)dy * dy;
|
||||||
|
s32 collDist = obj->isBomb ? BONUS_BOMB_COLLISION_DIST : BONUS_COLLISION_DIST;
|
||||||
|
if(distSq < collDist * collDist){
|
||||||
|
if(obj->isBomb){
|
||||||
|
sfxPlayerHit();
|
||||||
|
bonusExiting = TRUE;
|
||||||
|
bonusExitClock = 0;
|
||||||
|
if(bonusCursor) SPR_setVisibility(bonusCursor, HIDDEN);
|
||||||
|
for(u8 k = 0; k < BONUS_OBJ_COUNT; k++){
|
||||||
|
if(bonusObjs[k].active && bonusObjs[k].image){
|
||||||
|
SPR_releaseSprite(bonusObjs[k].image);
|
||||||
|
bonusObjs[k].image = NULL;
|
||||||
|
}
|
||||||
|
bonusObjs[k].active = FALSE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
bonusScoreVal += 512;
|
||||||
|
score += 512;
|
||||||
|
bonusStarCount++;
|
||||||
|
sfxPickup();
|
||||||
|
drawBonusStars();
|
||||||
|
}
|
||||||
|
SPR_releaseSprite(obj->image);
|
||||||
|
obj->image = NULL;
|
||||||
|
obj->active = FALSE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove if off-screen or scale fallback (for objects targeting near center)
|
||||||
|
if(sx < -16 || sx > 336 || sy < -16 || sy > 240 || obj->scale >= BONUS_KILL_SCALE){
|
||||||
|
SPR_releaseSprite(obj->image);
|
||||||
|
obj->image = NULL;
|
||||||
|
obj->active = FALSE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position sprite (center 16x16)
|
||||||
|
if(obj->image)
|
||||||
|
SPR_setPosition(obj->image, sx - 8, sy - 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Load / Update / Clear ---
|
||||||
|
|
||||||
|
void loadBonus(u8 variant){
|
||||||
|
// Use starfield system with bonusBg tiles, alternating between variants 0 and 1
|
||||||
|
(void)variant;
|
||||||
|
loadStarfield(0);
|
||||||
|
VDP_loadTileSet(&bonusBgTiles, STAR_TILE_I, DMA);
|
||||||
|
starAlternate = TRUE;
|
||||||
|
|
||||||
|
// Release the main game player sprite (hidden by clearLevel, but still allocated)
|
||||||
|
if(player.image){
|
||||||
|
SPR_releaseSprite(player.image);
|
||||||
|
player.image = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate cursor sprite — start at top of ring (270°)
|
||||||
|
bonusAngle = FIX16(270);
|
||||||
|
bonusAngleVel = 0;
|
||||||
|
bonusCursorX = BONUS_RING_CX + F16_toInt(F16_mul(F16_cos(bonusAngle), FIX16(BONUS_RING_RX)));
|
||||||
|
bonusCursorY = BONUS_RING_CY + F16_toInt(F16_mul(F16_sin(bonusAngle), FIX16(BONUS_RING_RY)));
|
||||||
|
bonusCursor = SPR_addSprite(&momoyoSprite, bonusCursorX - 24, bonusCursorY - 24,
|
||||||
|
TILE_ATTR(PAL0, FALSE, FALSE, FALSE));
|
||||||
|
if(bonusCursor) SPR_setVisibility(bonusCursor, VISIBLE);
|
||||||
|
|
||||||
|
// Init objects
|
||||||
|
for(u8 i = 0; i < BONUS_OBJ_COUNT; i++){
|
||||||
|
bonusObjs[i].active = FALSE;
|
||||||
|
bonusObjs[i].image = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init state
|
||||||
|
bonusScoreVal = 0;
|
||||||
|
bonusStarCount = 0;
|
||||||
|
bonusCl = 0;
|
||||||
|
bonusActive = TRUE;
|
||||||
|
|
||||||
|
// Init pattern sequencer
|
||||||
|
bonusBossNum = level / 3;
|
||||||
|
if(bonusBossNum > 4) bonusBossNum = 4;
|
||||||
|
bonusCurrentPattern = NULL;
|
||||||
|
bonusPatternIndex = 0;
|
||||||
|
bonusPatternDelay = 0;
|
||||||
|
bonusPatternBase = 0;
|
||||||
|
bonusPatternCooldown = tierCooldowns[bonusBossNum];
|
||||||
|
bonusSpiralDir = 1;
|
||||||
|
bonusSpiralPhase = 0;
|
||||||
|
bonusExiting = FALSE;
|
||||||
|
bonusExitClock = 0;
|
||||||
|
bonusPaused = FALSE;
|
||||||
|
bonusPauseInput = FALSE;
|
||||||
|
bonusPauseClock = 0;
|
||||||
|
|
||||||
|
// HUD on BG_A
|
||||||
|
drawBonusStars();
|
||||||
|
|
||||||
|
#if MUSIC_VOLUME > 0
|
||||||
|
XGM2_play(treasureMusic);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBonus(){
|
||||||
|
// --- Pause ---
|
||||||
|
if(!bonusExiting){
|
||||||
|
if(ctrl.start){
|
||||||
|
if(!bonusPauseInput){
|
||||||
|
bonusPauseInput = TRUE;
|
||||||
|
if(!bonusPaused){
|
||||||
|
bonusPaused = TRUE;
|
||||||
|
bonusPauseClock = 0;
|
||||||
|
if(bonusCursor) SPR_setPalette(bonusCursor, PAL1);
|
||||||
|
for(u8 i = 0; i < BONUS_OBJ_COUNT; i++)
|
||||||
|
if(bonusObjs[i].active && bonusObjs[i].image) SPR_setPalette(bonusObjs[i].image, PAL1);
|
||||||
|
XGM2_pause();
|
||||||
|
VDP_drawText("PAUSED", 17, BONUS_PAUSE_Y);
|
||||||
|
} else {
|
||||||
|
bonusPaused = FALSE;
|
||||||
|
if(bonusCursor) SPR_setPalette(bonusCursor, PAL0);
|
||||||
|
for(u8 i = 0; i < BONUS_OBJ_COUNT; i++)
|
||||||
|
if(bonusObjs[i].active && bonusObjs[i].image) SPR_setPalette(bonusObjs[i].image, PAL0);
|
||||||
|
XGM2_resume();
|
||||||
|
VDP_clearText(17, BONUS_PAUSE_Y, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bonusPauseInput = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(bonusPaused){
|
||||||
|
bonusPauseClock++;
|
||||||
|
if(bonusPauseClock % 60 < 30)
|
||||||
|
VDP_drawText("PAUSED", 17, BONUS_PAUSE_Y);
|
||||||
|
else
|
||||||
|
VDP_clearText(17, BONUS_PAUSE_Y, 6);
|
||||||
|
if(bonusPauseClock >= 240) bonusPauseClock = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bonusCl++;
|
||||||
|
|
||||||
|
// Alternate bg variant every 60 frames
|
||||||
|
if(starAlternate && bonusCl % 30 == 0)
|
||||||
|
starVariant ^= 1;
|
||||||
|
|
||||||
|
// Starfield animates only when not paused
|
||||||
|
updateStarfield();
|
||||||
|
|
||||||
|
// --- Exit sequence ---
|
||||||
|
if(bonusExiting){
|
||||||
|
bonusExitClock++;
|
||||||
|
if(bonusExitClock == 1){
|
||||||
|
VDP_drawText("BONUS FINISH", 13, 14);
|
||||||
|
char bonusStr[8];
|
||||||
|
uintToStr(bonusScoreVal, bonusStr, 1);
|
||||||
|
VDP_drawText("Bonus", 14, 16);
|
||||||
|
VDP_drawText(bonusStr, 20, 16);
|
||||||
|
sfxCollectAllTreasures();
|
||||||
|
}
|
||||||
|
if(bonusExitClock == 220)
|
||||||
|
PAL_fadeOut(0, 31, 20, TRUE);
|
||||||
|
if(bonusExitClock >= 240)
|
||||||
|
bonusActive = FALSE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Move cursor along elliptical ring (momentum-based) ---
|
||||||
|
if(ctrl.right || ctrl.left || ctrl.up || ctrl.down){
|
||||||
|
s16 ix = (ctrl.right ? 1 : 0) - (ctrl.left ? 1 : 0);
|
||||||
|
s16 iy = (ctrl.down ? 1 : 0) - (ctrl.up ? 1 : 0);
|
||||||
|
fix16 targetAngle = F16_atan2(FIX16(iy), FIX16(ix));
|
||||||
|
fix16 diff = targetAngle - bonusAngle;
|
||||||
|
if(diff > FIX16(180)) diff -= FIX16(360);
|
||||||
|
if(diff < FIX16(-180)) diff += FIX16(360);
|
||||||
|
// Determine target angular velocity from shortest-path direction
|
||||||
|
fix16 targetVel = (diff > 0) ? BONUS_ANGLE_SPEED : -BONUS_ANGLE_SPEED;
|
||||||
|
// Accelerate toward target velocity
|
||||||
|
if(bonusAngleVel < targetVel){
|
||||||
|
bonusAngleVel += BONUS_ANGLE_ACCEL;
|
||||||
|
if(bonusAngleVel > targetVel) bonusAngleVel = targetVel;
|
||||||
|
} else if(bonusAngleVel > targetVel){
|
||||||
|
bonusAngleVel -= BONUS_ANGLE_ACCEL;
|
||||||
|
if(bonusAngleVel < targetVel) bonusAngleVel = targetVel;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Friction: decelerate toward 0
|
||||||
|
if(bonusAngleVel > BONUS_ANGLE_FRICTION) bonusAngleVel -= BONUS_ANGLE_FRICTION;
|
||||||
|
else if(bonusAngleVel < -BONUS_ANGLE_FRICTION) bonusAngleVel += BONUS_ANGLE_FRICTION;
|
||||||
|
else bonusAngleVel = 0;
|
||||||
|
}
|
||||||
|
bonusAngle += bonusAngleVel;
|
||||||
|
bonusAngle = F16_normalizeAngle(bonusAngle);
|
||||||
|
s16 prevX = bonusCursorX;
|
||||||
|
bonusCursorX = BONUS_RING_CX + F16_toInt(F16_mul(F16_cos(bonusAngle), FIX16(BONUS_RING_RX)));
|
||||||
|
bonusCursorY = BONUS_RING_CY + F16_toInt(F16_mul(F16_sin(bonusAngle), FIX16(BONUS_RING_RY)));
|
||||||
|
if(bonusCursor){
|
||||||
|
if(bonusCursorX < prevX) SPR_setHFlip(bonusCursor, TRUE);
|
||||||
|
else if(bonusCursorX > prevX) SPR_setHFlip(bonusCursor, FALSE);
|
||||||
|
SPR_setPosition(bonusCursor, bonusCursorX - 24, bonusCursorY - 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pattern sequencer ---
|
||||||
|
if(bonusCurrentPattern){
|
||||||
|
if(bonusPatternDelay > 0){
|
||||||
|
bonusPatternDelay--;
|
||||||
|
} else {
|
||||||
|
const BonusPatternEntry* entry = &bonusCurrentPattern->entries[bonusPatternIndex];
|
||||||
|
fix16 angle = bonusPatternBase + FIX16(entry->angleOffset);
|
||||||
|
spawnBonusObjAt(angle, entry->isBomb);
|
||||||
|
bonusPatternIndex++;
|
||||||
|
if(bonusPatternIndex < bonusCurrentPattern->count){
|
||||||
|
bonusPatternDelay = bonusCurrentPattern->entries[bonusPatternIndex].delay;
|
||||||
|
} else {
|
||||||
|
bonusCurrentPattern = NULL;
|
||||||
|
bonusPatternCooldown = tierCooldowns[bonusBossNum];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(bonusPatternCooldown > 0){
|
||||||
|
bonusPatternCooldown--;
|
||||||
|
} else {
|
||||||
|
u8 poolCount = tierPoolCounts[bonusBossNum];
|
||||||
|
const BonusPattern* pat = tierPools[bonusBossNum][random() % poolCount];
|
||||||
|
bonusSpiralPhase++;
|
||||||
|
if(bonusSpiralPhase > 2){
|
||||||
|
bonusSpiralPhase = 0;
|
||||||
|
bonusSpiralDir *= -1;
|
||||||
|
}
|
||||||
|
bonusCurrentPattern = pat;
|
||||||
|
bonusPatternBase = FIX16(random() % 360);
|
||||||
|
bonusPatternIndex = 0;
|
||||||
|
bonusPatternDelay = pat->entries[0].delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update objects ---
|
||||||
|
for(u8 i = 0; i < BONUS_OBJ_COUNT; i++){
|
||||||
|
if(bonusObjs[i].active) updateBonusObj(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearBonus(){
|
||||||
|
// Release cursor sprite
|
||||||
|
if(bonusCursor){
|
||||||
|
SPR_releaseSprite(bonusCursor);
|
||||||
|
bonusCursor = NULL;
|
||||||
|
}
|
||||||
|
// Release object sprites
|
||||||
|
for(u8 i = 0; i < BONUS_OBJ_COUNT; i++){
|
||||||
|
if(bonusObjs[i].image){
|
||||||
|
SPR_releaseSprite(bonusObjs[i].image);
|
||||||
|
bonusObjs[i].image = NULL;
|
||||||
|
}
|
||||||
|
bonusObjs[i].active = FALSE;
|
||||||
|
}
|
||||||
|
// Clear starfield tiles from BG_B
|
||||||
|
clearStarfield();
|
||||||
|
// Clear bonus text from BG_A
|
||||||
|
VDP_clearTileMapRect(BG_A, 0, 0, 40, 32);
|
||||||
|
}
|
||||||
|
|
@ -367,6 +367,7 @@ void updateChrome(){
|
||||||
}
|
}
|
||||||
// level transition overlay
|
// level transition overlay
|
||||||
if(levelClearing){
|
if(levelClearing){
|
||||||
|
if(bonusStage) return;
|
||||||
if(levelClearClock == 2){
|
if(levelClearClock == 2){
|
||||||
char numStr[12];
|
char numStr[12];
|
||||||
char lvlStr[4];
|
char lvlStr[4];
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@ void sfxEnemyShotC();
|
||||||
void sfxExplosion();
|
void sfxExplosion();
|
||||||
void sfxPickup();
|
void sfxPickup();
|
||||||
void sfxGraze();
|
void sfxGraze();
|
||||||
|
void sfxPlayerHit();
|
||||||
|
void sfxCollectAllTreasures();
|
||||||
void loadMap();
|
void loadMap();
|
||||||
void loadGame();
|
void loadGame();
|
||||||
|
|
||||||
#define SKIP_START 1
|
#define SKIP_START 0
|
||||||
|
#define SKIP_TO_BONUS 0 // 1 = boot straight into bonus stage for testing (0 for release)
|
||||||
|
|
||||||
u32 clock;
|
u32 clock;
|
||||||
#define CLOCK_LIMIT 32000
|
#define CLOCK_LIMIT 32000
|
||||||
|
|
@ -69,7 +72,7 @@ u16 attractClock;
|
||||||
#define ATTRACT_LIMIT 900 // frames of title idle before attract triggers
|
#define ATTRACT_LIMIT 900 // frames of title idle before attract triggers
|
||||||
#define ATTRACT_DURATION 1800 // 30 seconds of demo gameplay
|
#define ATTRACT_DURATION 1800 // 30 seconds of demo gameplay
|
||||||
#define ATTRACT_LEVEL 1 // level index for attract mode (L12: Boss 4, 3 gunners)
|
#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)
|
||||||
// #define START_LEVEL 0 // offset added to starting level (0 = normal start)
|
// #define START_LEVEL 0 // offset added to starting level (0 = normal start)
|
||||||
s16 enemyCount, bulletCount;
|
s16 enemyCount, bulletCount;
|
||||||
u8 level;
|
u8 level;
|
||||||
|
|
@ -84,6 +87,7 @@ bool hitMessageBullet; // TRUE = blasted, FALSE = smashed
|
||||||
bool levelClearing;
|
bool levelClearing;
|
||||||
u32 levelClearClock;
|
u32 levelClearClock;
|
||||||
u8 levelWaitClock;
|
u8 levelWaitClock;
|
||||||
|
bool bonusStage;
|
||||||
|
|
||||||
// controls
|
// controls
|
||||||
struct controls {
|
struct controls {
|
||||||
|
|
|
||||||
48
src/main.c
48
src/main.c
|
|
@ -11,6 +11,7 @@
|
||||||
#include "chrome.h"
|
#include "chrome.h"
|
||||||
#include "start.h"
|
#include "start.h"
|
||||||
#include "starfield.h"
|
#include "starfield.h"
|
||||||
|
#include "bonus.h"
|
||||||
#include "sfx.h"
|
#include "sfx.h"
|
||||||
|
|
||||||
static void loadInternals(){
|
static void loadInternals(){
|
||||||
|
|
@ -70,7 +71,19 @@ void loadGame(){
|
||||||
killBullets = TRUE;
|
killBullets = TRUE;
|
||||||
attractEnding = FALSE;
|
attractEnding = FALSE;
|
||||||
started = TRUE;
|
started = TRUE;
|
||||||
|
#if SKIP_TO_BONUS
|
||||||
|
clearLevel();
|
||||||
|
loadBonus(level % 3);
|
||||||
|
bonusStage = TRUE;
|
||||||
|
levelClearing = TRUE;
|
||||||
|
levelClearClock = 1;
|
||||||
|
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);
|
||||||
|
#else
|
||||||
startLevelFadeIn();
|
startLevelFadeIn();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static void updateGame(){
|
static void updateGame(){
|
||||||
|
|
@ -94,11 +107,42 @@ static void updateGame(){
|
||||||
updateChrome();
|
updateChrome();
|
||||||
if(levelClearing){
|
if(levelClearing){
|
||||||
levelClearClock++;
|
levelClearClock++;
|
||||||
|
// Bonus stage branch (after boss fights)
|
||||||
|
if(bonusStage){
|
||||||
|
updateBonus();
|
||||||
|
if(!bonusActive){
|
||||||
|
bonusStage = FALSE;
|
||||||
|
clearBonus();
|
||||||
|
levelClearing = FALSE;
|
||||||
|
player.pos.y = FIX32(112);
|
||||||
|
player.camera = player.pos.x - FIX32(160);
|
||||||
|
playerVelX = 0;
|
||||||
|
loadBackground();
|
||||||
|
loadChrome();
|
||||||
|
loadLevel(level + 1);
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Starfield transition (non-boss levels)
|
||||||
if(levelClearClock == 73)
|
if(levelClearClock == 73)
|
||||||
XGM2_stop();
|
XGM2_stop();
|
||||||
if(levelClearClock == 1){
|
if(levelClearClock == 1){
|
||||||
clearLevel();
|
clearLevel();
|
||||||
loadStarfield(level % 3);
|
if(isBossLevel(level) && !isAttract){
|
||||||
|
loadBonus(level % 3);
|
||||||
|
bonusStage = TRUE;
|
||||||
|
} else {
|
||||||
|
loadStarfield(level % 3);
|
||||||
|
}
|
||||||
u16 transPal[32];
|
u16 transPal[32];
|
||||||
memcpy(transPal, font.palette->data, 16 * sizeof(u16));
|
memcpy(transPal, font.palette->data, 16 * sizeof(u16));
|
||||||
memcpy(transPal + 16, shadow.palette->data, 16 * sizeof(u16));
|
memcpy(transPal + 16, shadow.palette->data, 16 * sizeof(u16));
|
||||||
|
|
@ -125,7 +169,7 @@ static void updateGame(){
|
||||||
if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic);
|
if(!isAttract) XGM2_play(isBossLevel(level) ? bossMusic : stageMusic);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
if(levelClearing) updateStarfield();
|
if(levelClearing && !bonusStage) updateStarfield();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(levelWaitClock > 0){
|
if(levelWaitClock > 0){
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
#define CAMERA_W FIX32(208)
|
#define CAMERA_W FIX32(208)
|
||||||
|
|
||||||
#define SHOT_INTERVAL 20
|
#define SHOT_INTERVAL 20
|
||||||
#define PLAYER_SHOT_SPEED FIX32(18)
|
#define PLAYER_SHOT_SPEED FIX32(24)
|
||||||
|
|
||||||
s16 shotClock;
|
s16 shotClock;
|
||||||
fix32 screenX;
|
fix32 screenX;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ typedef struct {
|
||||||
static StarParticle stars[STAR_COUNT];
|
static StarParticle stars[STAR_COUNT];
|
||||||
static u8 spawnCounter;
|
static u8 spawnCounter;
|
||||||
static u8 starVariant;
|
static u8 starVariant;
|
||||||
|
static bool starAlternate; // when TRUE, spawnStar toggles variant 0/1
|
||||||
|
|
||||||
static void spawnStar(u8 i){
|
static void spawnStar(u8 i){
|
||||||
fix16 angleDeg = FIX16(random() % 360);
|
fix16 angleDeg = FIX16(random() % 360);
|
||||||
|
|
@ -56,6 +57,7 @@ static void preadvanceStar(u8 i){
|
||||||
|
|
||||||
void loadStarfield(u8 variant){
|
void loadStarfield(u8 variant){
|
||||||
starVariant = variant;
|
starVariant = variant;
|
||||||
|
starAlternate = FALSE;
|
||||||
VDP_loadTileSet(&starTiles, STAR_TILE_I, DMA);
|
VDP_loadTileSet(&starTiles, STAR_TILE_I, DMA);
|
||||||
// Reset BG_B scroll so stars appear at screen positions 0-39, 0-27
|
// Reset BG_B scroll so stars appear at screen positions 0-39, 0-27
|
||||||
VDP_setVerticalScroll(BG_B, 0);
|
VDP_setVerticalScroll(BG_B, 0);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue