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:
parent
3263b2597b
commit
073f96c9b1
25 changed files with 2320 additions and 1186 deletions
278
src/global.h
278
src/global.h
|
|
@ -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){
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue