// forward declarations (defined in sfx.h, chrome.h, main.c) void sfxPlayerShot(); void sfxEnemyShotA(); void sfxEnemyShotB(); void sfxEnemyShotC(); void sfxExplosion(); void sfxPickup(); void sfxGraze(); void sfxPlayerHit(); void sfxCollectAllTreasures(); void loadMap(); void loadGame(); #define SKIP_START 1 #define SKIP_TO_BONUS 0 // 1 = boot straight into bonus stage for testing (0 for release) u32 clock; #define CLOCK_LIMIT 32000 #define PROP_COUNT 8 #define GAME_H_F FIX32(224) #define SECTION_SIZE FIX32(512) #define SECTION_COUNT 3 #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 50 u32 score; u32 highScore; u32 tempHighScore; u32 grazeCount; bool levelPerfect; 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 #define MAP_X 1 #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 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; s16 pendingBossHp; s16 pendingBossNum; bool waitForRelease; s16 treasureCollectedClock; u8 treasureCollectedType; bool allTreasureCollected; u8 hitMessageClock; bool hitMessageBullet; // TRUE = blasted, FALSE = smashed bool levelClearing; u32 levelClearClock; u8 levelWaitClock; bool bonusStage; // controls 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){ ctrl.left = (state & BUTTON_LEFT); ctrl.right = (state & BUTTON_RIGHT); ctrl.up = (state & BUTTON_UP); ctrl.down = (state & BUTTON_DOWN); ctrl.a = (state & BUTTON_A); ctrl.b = (state & BUTTON_B); ctrl.c = (state & BUTTON_C); ctrl.start = (state & BUTTON_START); ctrlHW = ctrl; } } // player 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) u8 bombCount; // 0-2 u8 activePowerup; // 0=none, 1=spread, 2=rapid u16 powerupClock; // countdown bool hasShield; u16 shieldClock; // countdown (max 1800) fix32 camera; Sprite* image; }; struct playerStruct player; fix32 playerScrollVelY; // player.vel.y zeroed when clamped at top/bottom bound bool killBullets; // bullets #define BULLET_COUNT 70 struct bulletSpawner { fix32 x, y, speed; Vect2D_f32 vel; s16 angle, anim, frame; s16 ints[PROP_COUNT]; bool top, player; }; struct bullet { fix32 speed; bool active, player, vFlip, hFlip, 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]; // enemies #define ENEMY_COUNT 24 #define ENEMY_TYPE_ONE 0 #define ENEMY_TYPE_TWO 1 #define ENEMY_TYPE_THREE 2 #define ENEMY_TYPE_FOUR 3 #define ENEMY_TYPE_FIVE 4 #define ENEMY_TYPE_SIX 5 #define ENEMY_TYPE_SEVEN 6 #define ENEMY_TYPE_EIGHT 7 #define ENEMY_TYPE_NINE 8 #define ENEMY_TYPE_TEN 9 #define ENEMY_TYPE_ELEVEN 10 #define ENEMY_TYPE_TWELVE 11 #define ENEMY_TYPE_THIRTEEN 12 #define ENEMY_TYPE_FOURTEEN 13 #define ENEMY_TYPE_FIFTEEN 14 #define ENEMY_TYPE_SIXTEEN 15 #define ENEMY_TYPE_BOSS 16 #define ENEMY_TYPE_COUNT 16 // number of shoppable types (excludes boss) typedef struct { u8 type; // ENEMY_TYPE_* constant u8 cost; // TP cost u8 weight; // shopping probability weight u8 maxCount; // max per level u8 minCount; // guaranteed minimum per level u8 unlockLevel; // first level index where this type can appear } EnemyTypeDef; // cost: how many threat points this enemy costs to place (higher = fewer spawned) // weight: how likely this type is to be picked each shopping roll (higher = more common) // max: hard cap per level (won't exceed this even with remaining budget) // min: guaranteed spawns before shopping starts (cost deducted from budget) // unlock: first level index where this type enters the pool (0 = available immediately) static const EnemyTypeDef enemyTypeDefs[ENEMY_TYPE_COUNT] = { // cost weight max min unlock { ENEMY_TYPE_ONE, 1, 10, 2, 0, 5 }, { ENEMY_TYPE_TWO, 1, 10, 2, 0, 5 }, { ENEMY_TYPE_THREE, 1, 10, 2, 0, 5 }, { ENEMY_TYPE_FOUR, 1, 10, 2, 0, 5 }, { ENEMY_TYPE_FIVE, 1, 10, 2, 0, 5 }, { ENEMY_TYPE_SIX, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_SEVEN, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_EIGHT, 1, 10, 5, 0, 0 }, { ENEMY_TYPE_NINE, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_TEN, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_ELEVEN, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_TWELVE, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_THIRTEEN, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_FOURTEEN, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_FIFTEEN, 1, 10, 5, 0, 5 }, { ENEMY_TYPE_SIXTEEN, 1, 10, 5, 0, 5 }, }; // Threat point budget formula: base + (lvl * linear) + (lvl * lvl / quadratic) // Then randomized to 90-110%. Boss levels get bossPercent% of that. #define TP_BASE 8 #define TP_LINEAR 4 #define TP_QUADRATIC 5 #define TP_BOSS_PCT 40 struct enemy { bool active, onScreen; u8 type; s16 hp, frame, anim; s16 angle, off; u32 clock; fix32 speed; Vect2D_f32 vel, pos; Sprite* image; bool canGrabTreasure; bool homesOnPlayer; bool canShoot; bool canFlipH; bool useBigSprite; s16 carriedTreasure; s16 targetTreasure; s16 ints[PROP_COUNT]; fix16 fixes[PROP_COUNT]; }; struct enemy enemies[ENEMY_COUNT]; // treasure #define TREASURE_COUNT 8 #define TREASURE_WALKING 0 #define TREASURE_CARRIED 1 #define TREASURE_FALLING 2 #define TREASURE_COLLECTED 3 struct treasure { bool active; u8 state; u8 type; s16 carriedBy; s16 trailIndex; Vect2D_f32 pos, vel; Sprite* image; }; struct treasure treasures[TREASURE_COUNT]; bool treasureBeingCarried; s16 collectedCount; u16 levelEnemiesKilled; u16 statEnemiesKilled; s16 statTreasures; void removeShieldVisual(){ SPR_setDefinition(player.image, &momoyoSprite); } // pickups #define PICKUP_COUNT 1 #define PICKUP_TYPE_BOMB 0 #define PICKUP_TYPE_SPREAD 1 #define PICKUP_TYPE_RAPID 2 #define PICKUP_TYPE_SHIELD 3 #define PICKUP_SPAWN_INTERVAL 450 // ~15 sec (called on even frames only) #define PICKUP_LIFETIME 300 // ~10 sec (called on even frames only) #define PICKUP_BLINK_START 30 // blink final ~1 sec (even frames only) #define BOMB_MAX 2 #define BOMB_DAMAGE 8 #define BOMB_BOSS_DAMAGE 4 #define BOMB_IFRAMES 60 #define BOMB_BULLET_SCORE 32 #define POWERUP_DURATION 600 #define SHIELD_TIMEOUT 600 #define PICKUP_OFF 8 struct pickup { bool active; u8 type; u16 lifeClock; Vect2D_f32 pos; Sprite* image; }; struct pickup pickups[PICKUP_COUNT]; u16 pickupSpawnClock; void killPickup(u8 i){ if(!pickups[i].active) return; pickups[i].active = FALSE; SPR_releaseSprite(pickups[i].image); } static fix32 getWrappedDelta(fix32 a, fix32 b) { fix32 delta = a - b; if (delta > GAME_WRAP / 2) { delta -= GAME_WRAP; } else if (delta < -GAME_WRAP / 2) { delta += GAME_WRAP; } return delta; } static s16 getScreenX(fix32 worldX, fix32 camera) { fix32 screenX = worldX - camera; if (screenX < -(GAME_WRAP / 2)) { screenX += GAME_WRAP; } else if (screenX > (GAME_WRAP / 2)) { screenX -= GAME_WRAP; } return F32_toInt(screenX); } void killTreasure(u8 i){ if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){ enemies[treasures[i].carriedBy].carriedTreasure = -1; treasureBeingCarried = FALSE; } treasures[i].active = FALSE; SPR_releaseSprite(treasures[i].image); } // explosion pool (shared by all explosions: bullet, enemy, player death) #define EXPLOSION_COUNT 12 struct explosion { bool active, big; u8 frame, clock; fix32 x, y; Sprite* image; }; struct explosion explosions[EXPLOSION_COUNT]; void spawnExplosion(fix32 x, fix32 y, u8 anim, bool big){ fix32 dx = getWrappedDelta(x, player.pos.x); if(dx < -CULL_LIMIT || dx > CULL_LIMIT) return; s16 slot = -1; for(s16 j = 0; j < EXPLOSION_COUNT; j++) if(!explosions[j].active){ slot = j; break; } if(slot < 0) return; explosions[slot].active = TRUE; explosions[slot].big = big; explosions[slot].x = x; explosions[slot].y = y; explosions[slot].frame = 0; explosions[slot].clock = 0; SpriteDefinition const* def = big ? &explosionBigSprite : &explosionsSprite; explosions[slot].image = SPR_addSprite(def, -64, -64, TILE_ATTR(PAL0, 0, 0, 0)); if(!explosions[slot].image){ explosions[slot].active = FALSE; return; } SPR_setDepth(explosions[slot].image, big ? 0 : 5); SPR_setAnim(explosions[slot].image, anim); SPR_setFrame(explosions[slot].image, 0); SPR_setHFlip(explosions[slot].image, random() & 1); s16 sx = getScreenX(x, player.camera); s16 sy = F32_toInt(y); u8 off = big ? 32 : 16; SPR_setPosition(explosions[slot].image, sx - off, sy - off); } void updateExplosions(){ for(s16 i = 0; i < EXPLOSION_COUNT; i++){ if(!explosions[i].active) continue; explosions[i].clock++; if(explosions[i].clock % 4 == 0){ explosions[i].frame++; if(explosions[i].frame >= 5){ SPR_releaseSprite(explosions[i].image); explosions[i].active = FALSE; continue; } SPR_setFrame(explosions[i].image, explosions[i].frame); } s16 sx = getScreenX(explosions[i].x, player.camera); s16 sy = F32_toInt(explosions[i].y); u8 off = explosions[i].big ? 32 : 16; SPR_setPosition(explosions[i].image, sx - off, sy - off); } } void clearExplosions(){ for(s16 i = 0; i < EXPLOSION_COUNT; i++){ if(explosions[i].active){ SPR_releaseSprite(explosions[i].image); explosions[i].active = FALSE; } } } static u8 getBulletExplosionAnim(u8 i){ if(bullets[i].player) return 3; // yellow if(bullets[i].anim < FIRST_ROTATING_BULLET) return bullets[i].frame; // 0=blue, 1=red, 2=green return (bullets[i].anim - FIRST_ROTATING_BULLET) % 3; // rotating: 0=blue, 1=red, 2=green } void killBullet(u8 i, bool explode){ if(explode){ spawnExplosion(bullets[i].pos.x, bullets[i].pos.y, getBulletExplosionAnim(i), FALSE); } bullets[i].active = FALSE; SPR_releaseSprite(bullets[i].image); } void killEnemy(u8 i){ if(isAttract) return; enemies[i].hp--; if(enemies[i].hp > 0){ // enemy hit but not dead — small yellow explosion spawnExplosion(enemies[i].pos.x, enemies[i].pos.y, 3, FALSE); return; } // enemy killed — big explosion spawnExplosion(enemies[i].pos.x, enemies[i].pos.y, 3, TRUE); if(enemies[i].carriedTreasure >= 0){ s16 h = enemies[i].carriedTreasure; if(treasures[h].active){ treasures[h].state = TREASURE_FALLING; treasures[h].carriedBy = -1; treasures[h].vel.x = 0; treasures[h].vel.y = FIX32(3); } treasureBeingCarried = FALSE; } enemies[i].active = FALSE; SPR_releaseSprite(enemies[i].image); levelEnemiesKilled++; } // 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))); } // 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; } static bool isBossLevel(u8 lvl){ return (lvl >= 2) && ((lvl + 1) % 3 == 0); } #define FONT_THEME_RED 0 #define FONT_THEME_GREEN 1 #define FONT_THEME_BLUE 2 u16 fontPal[16]; void loadFontPalette(u8 theme) { u16 coloredPalette[16]; u8 i; for(i = 0; i < 16; i++) { u16 color = font.palette->data[i]; u16 r = color & 0xF; u16 g = (color >> 4) & 0xF; u16 b = (color >> 8) & 0xF; switch(theme) { case FONT_THEME_GREEN: coloredPalette[i] = (b << 8) | (r << 4) | g; break; case FONT_THEME_BLUE: { u16 newB = r > b ? r : b; coloredPalette[i] = (newB << 8) | (g << 4) | (r >> 1); break; } default: // FONT_THEME_RED coloredPalette[i] = color; break; } } memcpy(fontPal, coloredPalette, 16 * sizeof(u16)); PAL_setPalette(PAL3, coloredPalette, DMA_QUEUE); VDP_setTextPalette(PAL3); }