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

@ -8,6 +8,66 @@
u16 hudPal = PAL0;
#define POPUP_COUNT 4
struct scorePopup {
bool active;
u8 clock;
u8 len;
s16 tileX, tileY;
char text[6];
};
struct scorePopup popups[POPUP_COUNT];
void spawnPopup(fix32 worldX, fix32 worldY, u32 value){
s16 slot = -1;
for(s16 i = 0; i < POPUP_COUNT; i++) if(!popups[i].active){ slot = i; break; }
if(slot == -1) return;
s16 screenX = getScreenX(worldX, player.camera);
s16 screenY = F32_toInt(worldY);
s16 tX = screenX / 8;
s16 tY = screenY / 8;
tX--;
if(tX < 0) tX = 0;
if(tX > 38) tX = 38;
if(tY < 6) tY = 6;
if(tY > 25) tY = 25;
popups[slot].tileX = tX;
popups[slot].tileY = tY;
uintToStr(value, popups[slot].text, 1);
popups[slot].len = strlen(popups[slot].text);
popups[slot].clock = 0;
popups[slot].active = TRUE;
bigText(popups[slot].text, tX, tY, TRUE);
}
static void updatePopups(){
for(s16 i = 0; i < POPUP_COUNT; i++){
if(!popups[i].active) continue;
popups[i].clock++;
if(popups[i].clock >= 24){
VDP_clearTileMapRect(BG_A, popups[i].tileX, popups[i].tileY, popups[i].len, 2);
popups[i].active = FALSE;
continue;
}
if(popups[i].clock % 8 == 0){
VDP_clearTileMapRect(BG_A, popups[i].tileX, popups[i].tileY, popups[i].len, 2);
popups[i].tileY--;
if(popups[i].tileY < 6) popups[i].tileY = 6;
bigText(popups[i].text, popups[i].tileX, popups[i].tileY, TRUE);
}
}
}
void clearPopups(){
for(s16 i = 0; i < POPUP_COUNT; i++){
if(popups[i].active){
VDP_clearTileMapRect(BG_A, popups[i].tileX, popups[i].tileY, popups[i].len, 2);
popups[i].active = FALSE;
}
}
}
#define FONT_BIG_I 340
void bigText(char* str, u16 x, u16 y, bool shadow){
@ -185,6 +245,10 @@ static void updateMap(){
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow);
}
// pickup HUD tracking
s16 lastBombCount = -1;
s16 lastPowerupState = -1; // 0=none, 1=spread, 2=rapid, 3=shield (composite)
u8 phraseIndex[4];
s16 lastLevel;
@ -221,11 +285,42 @@ static void repaintMap(){
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + mapPlayerRow);
}
static void drawBombCount(){
if(isAttract) return;
if(player.bombCount > 0){
char bStr[4] = "B:";
char numStr[2];
uintToStr(player.bombCount, numStr, 1);
bStr[2] = numStr[0];
bStr[3] = 0;
VDP_drawText(bStr, 1, 7);
} else {
VDP_clearText(1, 7, 3);
}
lastBombCount = player.bombCount;
}
static void drawPowerupIndicator(){
if(isAttract) return;
VDP_clearText(1, 8, 6);
if(player.hasShield)
VDP_drawText("SH", 1, 8);
else if(player.activePowerup == 1)
VDP_drawText("SPREAD", 1, 8);
else if(player.activePowerup == 2)
VDP_drawText("RAPID", 1, 8);
s16 state = player.activePowerup;
if(player.hasShield) state = 3;
lastPowerupState = state;
}
static void repaintHud(){
bigText(scoreStr, SCORE_X, SCORE_Y, FALSE);
drawLives();
repaintMap();
drawLevel();
drawBombCount();
drawPowerupIndicator();
}
void loadChrome(){
@ -263,7 +358,7 @@ static void doGameOver(){
void noop(s16 j){ (void)j; }
spawnBullet(spawner, noop);
for(s16 j = BULLET_COUNT - 1; j >= 0; j--){
if(bullets[j].active && !bullets[j].explosion
if(bullets[j].active
&& bullets[j].pos.x == treasures[i].pos.x && bullets[j].pos.y == treasures[i].pos.y){
killBullet(j, TRUE);
break;
@ -273,6 +368,7 @@ static void doGameOver(){
killTreasure(i);
}
SPR_releaseSprite(player.image);
clearPickups();
// clear lives
VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16);
@ -297,6 +393,7 @@ static void showPause(){
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL1);
for(s16 i = 0; i < PICKUP_COUNT; i++) if(pickups[i].active) SPR_setPalette(pickups[i].image, PAL1);
SPR_setPalette(player.image, PAL1);
hudPal = PAL1;
hudPal = PAL1;
@ -309,6 +406,7 @@ static void clearPause(){
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL0);
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0);
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0);
for(s16 i = 0; i < PICKUP_COUNT; i++) if(pickups[i].active) SPR_setPalette(pickups[i].image, PAL0);
SPR_setPalette(player.image, PAL0);
hudPal = PAL0;
repaintHud();
@ -345,12 +443,13 @@ static void updatePause(){
}
#define TRANSITION_TREASURE_X 10
#define TRANSITION_TREASURE_Y 13
#define TRANSITION_TREASURE_Y 15
#define TRANSITION_LEVEL_X 12
#define TRANSITION_LEVEL_Y 15
#define TRANSITION_LEVEL_Y 13
void updateChrome(){
updatePopups();
updatePause();
if(gameOver && !didGameOver) doGameOver();
if(didGameOver){
@ -396,12 +495,31 @@ void updateChrome(){
else
VDP_drawText("Lives Left", TRANSITION_LEVEL_X + 2, TRANSITION_LEVEL_Y + 5);
if(grazeCount > 0){
char grazeStr[8];
char grazePtsStr[12];
uintToStr(grazeCount, grazeStr, 1);
uintToStr(grazeCount * 64, grazePtsStr, 1);
VDP_drawText("Grazes", TRANSITION_LEVEL_X, TRANSITION_LEVEL_Y + 7);
VDP_drawText(grazeStr, TRANSITION_LEVEL_X + 7, TRANSITION_LEVEL_Y + 7);
VDP_drawText(grazePtsStr, TRANSITION_LEVEL_X + 7 + strlen(grazeStr) + 1, TRANSITION_LEVEL_Y + 7);
VDP_drawText("pts", TRANSITION_LEVEL_X + 7 + strlen(grazeStr) + 1 + strlen(grazePtsStr) + 1, TRANSITION_LEVEL_Y + 7);
}
if(levelPerfect){
score += 4096;
lastScore = score;
VDP_drawText("PERFECT! +4096", 13, TRANSITION_LEVEL_Y + 9);
}
}
if(levelClearClock >= 230){
VDP_clearText(0, TRANSITION_TREASURE_Y, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 3, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 5, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 7, 40);
VDP_clearText(0, TRANSITION_LEVEL_Y + 9, 40);
}
return;
}
@ -430,6 +548,7 @@ void updateChrome(){
}
if(allDone && collectedCount > 0){
allTreasureCollected = TRUE;
score += 4096;
VDP_drawText("All Treasure Found!", 11, 5);
} else {
const char* mirrorPhrases[] = {"Reflect the Depths", "Dig Deeper Within", "See What Shines Below", "Mirror of the Mine", "Look Back, Strike Back"};
@ -466,5 +585,12 @@ void updateChrome(){
allTreasureCollected = FALSE;
VDP_drawText("All Enemies Down!", 12, 5);
}
// pickup HUD
if(!isAttract){
if(lastBombCount != player.bombCount) drawBombCount();
s16 curPowerup = player.activePowerup;
if(player.hasShield) curPowerup = 3;
if(lastPowerupState != curPowerup) drawPowerupIndicator();
}
if(clock % 4 == 0) updateMap();
}