- Add pickup system (bomb, spread, rapid, shield) with new sprites - Replace Docker build with native SGDK compile via m68k-elf-gcc - Rework enemy spawning, homing math, boss HP/number globals - Expand chrome: score popups, minimap, pause/game over improvements - Overhaul stage generation with threat-point system - Add explosion sprites, shield sprite, powerup sprite - Add tools/ for sprite downscaling utilities
483 lines
No EOL
13 KiB
C
483 lines
No EOL
13 KiB
C
// 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);
|
|
} |