bonus stage

This commit is contained in:
t. boddy 2026-03-20 09:33:18 -04:00
parent a8bc01bedd
commit 3263b2597b
11 changed files with 529 additions and 6 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
CLAUDE.md
*.md
.DS_Store
**/.DS_Store
Thumbs.db

BIN
res/bonus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
res/bonusbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -31,6 +31,9 @@ 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
SPRITE bonusObjSprite "bonus.png" 2 2 NONE 0
TILESET bonusBgTiles "bonusbg.png" NONE
IMAGE mapIndicator "mapindicator.png" NONE NONE
TILESET starTiles "stars.png" NONE

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

469
src/bonus.h Normal file
View 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);
}

View file

@ -367,6 +367,7 @@ void updateChrome(){
}
// level transition overlay
if(levelClearing){
if(bonusStage) return;
if(levelClearClock == 2){
char numStr[12];
char lvlStr[4];

View file

@ -6,10 +6,13 @@ void sfxEnemyShotC();
void sfxExplosion();
void sfxPickup();
void sfxGraze();
void sfxPlayerHit();
void sfxCollectAllTreasures();
void loadMap();
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;
#define CLOCK_LIMIT 32000
@ -69,7 +72,7 @@ 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)
// #define START_LEVEL 0 // offset added to starting level (0 = normal start)
s16 enemyCount, bulletCount;
u8 level;
@ -84,6 +87,7 @@ bool hitMessageBullet; // TRUE = blasted, FALSE = smashed
bool levelClearing;
u32 levelClearClock;
u8 levelWaitClock;
bool bonusStage;
// controls
struct controls {

View file

@ -11,6 +11,7 @@
#include "chrome.h"
#include "start.h"
#include "starfield.h"
#include "bonus.h"
#include "sfx.h"
static void loadInternals(){
@ -70,7 +71,19 @@ void loadGame(){
killBullets = TRUE;
attractEnding = FALSE;
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();
#endif
}
static void updateGame(){
@ -94,11 +107,42 @@ static void updateGame(){
updateChrome();
if(levelClearing){
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)
XGM2_stop();
if(levelClearClock == 1){
clearLevel();
loadStarfield(level % 3);
if(isBossLevel(level) && !isAttract){
loadBonus(level % 3);
bonusStage = TRUE;
} else {
loadStarfield(level % 3);
}
u16 transPal[32];
memcpy(transPal, font.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);
#endif
}
if(levelClearing) updateStarfield();
if(levelClearing && !bonusStage) updateStarfield();
return;
}
if(levelWaitClock > 0){

View file

@ -10,7 +10,7 @@
#define CAMERA_W FIX32(208)
#define SHOT_INTERVAL 20
#define PLAYER_SHOT_SPEED FIX32(18)
#define PLAYER_SHOT_SPEED FIX32(24)
s16 shotClock;
fix32 screenX;

View file

@ -22,6 +22,7 @@ typedef struct {
static StarParticle stars[STAR_COUNT];
static u8 spawnCounter;
static u8 starVariant;
static bool starAlternate; // when TRUE, spawnStar toggles variant 0/1
static void spawnStar(u8 i){
fix16 angleDeg = FIX16(random() % 360);
@ -56,6 +57,7 @@ static void preadvanceStar(u8 i){
void loadStarfield(u8 variant){
starVariant = variant;
starAlternate = FALSE;
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);