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

@ -42,11 +42,13 @@ void spawnEnemy(u8 type, u8 zone){
enemies[i].pos.x = randX;
enemies[i].pos.y = randY;
// Default sprite — load functions can override via SPR_setDefinition()
static const SpriteDefinition* bossSpriteDefs[4] = { &boss1Sprite, &boss2Sprite, &boss3Sprite, &boss4Sprite };
SpriteDefinition const* spriteDef;
if(type == ENEMY_TYPE_DRONE) spriteDef = &eyeBigSprite;
else if(type == ENEMY_TYPE_BOSS) spriteDef = bossSpriteDefs[pendingBossNum % 4];
if(type == ENEMY_TYPE_BOSS) spriteDef = bossSpriteDefs[pendingBossNum % 4];
else spriteDef = &fairySprite;
enemies[i].off = (type == ENEMY_TYPE_BOSS) ? 24 : 16;
enemies[i].image = SPR_addSprite(spriteDef,
getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, F32_toInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0));
@ -61,23 +63,62 @@ void spawnEnemy(u8 type, u8 zone){
enemies[i].ints[j] = 0;
enemies[i].fixes[j] = 0;
}
enemies[i].ints[3] = -1;
enemies[i].canGrabTreasure = FALSE;
enemies[i].homesOnPlayer = FALSE;
enemies[i].canShoot = FALSE;
enemies[i].canFlipH = FALSE;
enemies[i].useBigSprite = FALSE;
enemies[i].carriedTreasure = -1;
enemies[i].targetTreasure = -1;
enemies[i].anim = 0;
switch(enemies[i].type){
case ENEMY_TYPE_TEST:
case ENEMY_TYPE_ONE:
loadEnemyOne(i);
break;
case ENEMY_TYPE_DRONE:
loadDrone(i);
case ENEMY_TYPE_TWO:
loadEnemyTwo(i);
break;
case ENEMY_TYPE_GUNNER:
loadGunner(i);
case ENEMY_TYPE_THREE:
loadEnemyThree(i);
break;
case ENEMY_TYPE_HUNTER:
loadHunter(i);
case ENEMY_TYPE_FOUR:
loadEnemyFour(i);
break;
case ENEMY_TYPE_BUILDER:
loadBuilder(i);
case ENEMY_TYPE_FIVE:
loadEnemyFive(i);
break;
case ENEMY_TYPE_SIX:
loadEnemySix(i);
break;
case ENEMY_TYPE_SEVEN:
loadEnemySeven(i);
break;
case ENEMY_TYPE_EIGHT:
loadEnemyEight(i);
break;
case ENEMY_TYPE_NINE:
loadEnemyNine(i);
break;
case ENEMY_TYPE_TEN:
loadEnemyTen(i);
break;
case ENEMY_TYPE_ELEVEN:
loadEnemyEleven(i);
break;
case ENEMY_TYPE_TWELVE:
loadEnemyTwelve(i);
break;
case ENEMY_TYPE_THIRTEEN:
loadEnemyThirteen(i);
break;
case ENEMY_TYPE_FOURTEEN:
loadEnemyFourteen(i);
break;
case ENEMY_TYPE_FIFTEEN:
loadEnemyFifteen(i);
break;
case ENEMY_TYPE_SIXTEEN:
loadEnemySixteen(i);
break;
case ENEMY_TYPE_BOSS:
loadBoss(i);
@ -89,11 +130,11 @@ void spawnEnemy(u8 type, u8 zone){
}
static void boundsEnemy(u8 i){
if((enemies[i].type == ENEMY_TYPE_TEST || enemies[i].type == ENEMY_TYPE_BUILDER) && enemies[i].ints[3] >= 0){
s16 h = enemies[i].ints[3];
if(enemies[i].canGrabTreasure && enemies[i].carriedTreasure >= 0){
s16 h = enemies[i].carriedTreasure;
// if the treasure was collected by player or gone, kill this enemy
if(!treasures[h].active || treasures[h].state == TREASURE_COLLECTED){
enemies[i].ints[3] = -1;
enemies[i].carriedTreasure = -1;
treasureBeingCarried = FALSE;
killEnemy(i);
return;
@ -108,17 +149,13 @@ static void boundsEnemy(u8 i){
treasures[h].vel.x = 0;
treasures[h].vel.y = FIX32(3);
}
enemies[i].ints[3] = -1;
enemies[i].carriedTreasure = -1;
treasureBeingCarried = FALSE;
enemies[i].vel.y = FIX32(1);
} else {
if(treasures[h].active) killTreasure(h);
enemies[i].ints[3] = -1;
enemies[i].carriedTreasure = -1;
treasureBeingCarried = FALSE;
if(enemies[i].type == ENEMY_TYPE_BUILDER){
u8 zone = F32_toInt(enemies[i].pos.x) / 512;
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
}
enemies[i].hp = 0;
killEnemy(i);
}
@ -143,6 +180,64 @@ static void boundsEnemy(u8 i){
}
}
static void enemySeekTreasure(u8 i){
// carrying: steer upward
if(enemies[i].carriedTreasure >= 0){
enemies[i].angle = FIX16(248 + (random() % 45));
enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
return;
}
// cancel target if a treasure is already being carried
if(treasureBeingCarried && enemies[i].targetTreasure >= 0){
enemies[i].targetTreasure = -1;
}
// scan for nearest walking treasure every 30 frames
if(!treasureBeingCarried && enemies[i].clock % 30 == 0){
s16 bestTreasure = -1;
fix32 bestDist = FIX32(9999);
for(s16 j = 0; j < TREASURE_COUNT; j++){
if(!treasures[j].active || treasures[j].state != TREASURE_WALKING) continue;
fix32 dx = getWrappedDelta(enemies[i].pos.x, treasures[j].pos.x);
fix32 dy = enemies[i].pos.y - treasures[j].pos.y;
fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
if(dist < bestDist && dist < FIX32(256)){
bestDist = dist;
bestTreasure = j;
}
}
enemies[i].targetTreasure = bestTreasure;
}
// steer toward target treasure
if(enemies[i].targetTreasure >= 0){
s16 t = enemies[i].targetTreasure;
if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){
enemies[i].targetTreasure = -1;
} else {
fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x);
fix32 dy = treasures[t].pos.y - enemies[i].pos.y;
fix16 angle = getAngle(dx, dy);
enemies[i].vel.x = F32_mul(F32_cos(angle), enemies[i].speed);
enemies[i].vel.y = F32_mul(F32_sin(angle), enemies[i].speed);
// grab check: within 16px
fix32 adx = dx < 0 ? -dx : dx;
fix32 ady = dy < 0 ? -dy : dy;
if(adx < FIX32(16) && ady < FIX32(16)){
enemies[i].carriedTreasure = t;
enemies[i].targetTreasure = -1;
treasureBeingCarried = TRUE;
treasures[t].state = TREASURE_CARRIED;
treasures[t].carriedBy = i;
}
}
}
}
static void updateEnemy(u8 i){
enemies[i].pos.x += enemies[i].vel.x - (player.vel.x >> 3);
enemies[i].pos.y += enemies[i].vel.y - (playerScrollVelY >> 3);
@ -153,21 +248,67 @@ static void updateEnemy(u8 i){
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
enemies[i].onScreen = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
// flag-based treasure seeking (before type-specific update)
if(enemies[i].canGrabTreasure){
enemySeekTreasure(i);
}
// flag-based homing
if(enemies[i].homesOnPlayer){
enemies[i].angle = enemyHoneAngle(i);
if(player.respawnClock > 0) enemies[i].angle = F16_normalizeAngle(enemies[i].angle + FIX16(180));
enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
}
switch(enemies[i].type){
case ENEMY_TYPE_TEST:
case ENEMY_TYPE_ONE:
updateEnemyOne(i);
break;
case ENEMY_TYPE_DRONE:
updateDrone(i);
case ENEMY_TYPE_TWO:
updateEnemyTwo(i);
break;
case ENEMY_TYPE_GUNNER:
updateGunner(i);
case ENEMY_TYPE_THREE:
updateEnemyThree(i);
break;
case ENEMY_TYPE_HUNTER:
updateHunter(i);
case ENEMY_TYPE_FOUR:
updateEnemyFour(i);
break;
case ENEMY_TYPE_BUILDER:
updateBuilder(i);
case ENEMY_TYPE_FIVE:
updateEnemyFive(i);
break;
case ENEMY_TYPE_SIX:
updateEnemySix(i);
break;
case ENEMY_TYPE_SEVEN:
updateEnemySeven(i);
break;
case ENEMY_TYPE_EIGHT:
updateEnemyEight(i);
break;
case ENEMY_TYPE_NINE:
updateEnemyNine(i);
break;
case ENEMY_TYPE_TEN:
updateEnemyTen(i);
break;
case ENEMY_TYPE_ELEVEN:
updateEnemyEleven(i);
break;
case ENEMY_TYPE_TWELVE:
updateEnemyTwelve(i);
break;
case ENEMY_TYPE_THIRTEEN:
updateEnemyThirteen(i);
break;
case ENEMY_TYPE_FOURTEEN:
updateEnemyFourteen(i);
break;
case ENEMY_TYPE_FIFTEEN:
updateEnemyFifteen(i);
break;
case ENEMY_TYPE_SIXTEEN:
updateEnemySixteen(i);
break;
case ENEMY_TYPE_BOSS:
updateBoss(i);
@ -180,46 +321,43 @@ static void updateEnemy(u8 i){
fix32 edy = enemies[i].pos.y - player.pos.y;
if(edx >= FIX32(-16) && edx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){
sfxExplosion();
// spawn explosion at player position
s16 expSlot = -1;
for(s16 j = 0; j < BULLET_COUNT; j++) if(!bullets[j].active){ expSlot = j; break; }
if(expSlot >= 0){
bullets[expSlot].active = TRUE;
bullets[expSlot].player = TRUE;
bullets[expSlot].explosion = TRUE;
bullets[expSlot].pos.x = player.pos.x;
bullets[expSlot].pos.y = player.pos.y;
bullets[expSlot].vel.x = 0;
bullets[expSlot].vel.y = 0;
bullets[expSlot].clock = 0;
bullets[expSlot].frame = 0;
bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0));
if(bullets[expSlot].image){
SPR_setDepth(bullets[expSlot].image, 5);
SPR_setAnim(bullets[expSlot].image, 1);
SPR_setFrame(bullets[expSlot].image, 0);
SPR_setHFlip(bullets[expSlot].image, random() & 1);
} else {
bullets[expSlot].active = FALSE;
}
}
// spawn big explosion at player position
spawnExplosion(player.pos.x, player.pos.y, 3, TRUE); // yellow
if(enemies[i].type != ENEMY_TYPE_BOSS){
enemies[i].hp = 0;
killEnemy(i);
}
if(!isAttract){
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
sfxGameOver();
} else {
sfxPlayerHit();
player.respawnClock = 120;
SPR_setVisibility(player.image, HIDDEN);
if(player.hasShield){
// shield absorbs hit
player.hasShield = FALSE;
player.shieldClock = 0;
removeShieldVisual();
player.recoveringClock = 60;
player.recoverFlash = TRUE;
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = FALSE;
} else {
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
sfxGameOver();
} else {
sfxPlayerHit();
levelPerfect = FALSE;
player.respawnClock = 120;
player.activePowerup = 0;
player.powerupClock = 0;
player.hasShield = FALSE;
player.shieldClock = 0;
removeShieldVisual();
SPR_setVisibility(player.image, HIDDEN);
killBullets = TRUE;
hitMessageClock = 120;
hitMessageBullet = FALSE;
}
}
}
}
@ -228,7 +366,7 @@ static void updateEnemy(u8 i){
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
s16 sy = F32_toInt(enemies[i].pos.y);
SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN);
if(enemies[i].type != ENEMY_TYPE_DRONE && enemies[i].type != ENEMY_TYPE_BOSS)
if(enemies[i].canFlipH)
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0);
SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off);