pickups, native build, enemy/bullet/stage overhaul

- 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
This commit is contained in:
t. boddy 2026-04-15 08:19:29 -04:00
parent 3263b2597b
commit 073f96c9b1
25 changed files with 2320 additions and 1186 deletions

View file

@ -11,7 +11,7 @@ void sfxCollectAllTreasures();
void loadMap();
void loadGame();
#define SKIP_START 0
#define SKIP_START 1
#define SKIP_TO_BONUS 0 // 1 = boot straight into bonus stage for testing (0 for release)
u32 clock;
@ -21,19 +21,20 @@ u32 clock;
#define GAME_H_F FIX32(224)
#define SECTION_SIZE FIX32(512)
#define SECTION_COUNT 4
#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 0
#define MUSIC_VOLUME 50
u32 score;
u32 highScore;
u32 tempHighScore;
u32 grazeCount;
bool levelPerfect;
u32 nextExtendScore;
#define EXTEND_SCORE 25000
#define SCORE_LENGTH 8
@ -118,6 +119,11 @@ struct playerStruct {
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;
};
@ -138,7 +144,7 @@ struct bulletSpawner {
};
struct bullet {
fix32 speed;
bool active, player, vFlip, hFlip, explosion, grazed;
bool active, player, vFlip, hFlip, grazed;
Vect2D_f32 pos, vel;
Sprite* image;
s16 clock, angle, anim, frame;
@ -152,12 +158,66 @@ struct bullet bullets[BULLET_COUNT];
// enemies
#define ENEMY_COUNT 24
#define ENEMY_TYPE_TEST 0
#define ENEMY_TYPE_DRONE 1
#define ENEMY_TYPE_GUNNER 2
#define ENEMY_TYPE_HUNTER 3
#define ENEMY_TYPE_BUILDER 4
#define ENEMY_TYPE_BOSS 5
#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;
@ -168,6 +228,13 @@ struct enemy {
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];
};
@ -196,57 +263,36 @@ u16 levelEnemiesKilled;
u16 statEnemiesKilled;
s16 statTreasures;
void killTreasure(u8 i){
if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){
enemies[treasures[i].carriedBy].ints[3] = -1;
treasureBeingCarried = FALSE;
}
treasures[i].active = FALSE;
SPR_releaseSprite(treasures[i].image);
void removeShieldVisual(){
SPR_setDefinition(player.image, &momoyoSprite);
}
void killBullet(u8 i, bool explode){
if(explode){
s16 a = bullets[i].anim;
s16 explosionAnim;
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;
SPR_setFrame(bullets[i].image, 0);
SPR_setHFlip(bullets[i].image, random() & 1);
// SPR_setVFlip(bullets[i].image, random() & 1);
} else {
bullets[i].active = FALSE;
SPR_releaseSprite(bullets[i].image);
}
}
// 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
void killEnemy(u8 i){
if(isAttract) return;
enemies[i].hp--;
if(enemies[i].hp > 0) return;
if(enemies[i].ints[3] >= 0){
s16 h = enemies[i].ints[3];
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++;
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) {
@ -269,6 +315,122 @@ static s16 getScreenX(fix32 worldX, fix32 camera) {
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){