lots of work

This commit is contained in:
t. boddy 2026-02-18 17:58:41 -05:00
parent 4036b5f07e
commit 06648c2dc1
18 changed files with 640 additions and 113 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

@ -31,3 +31,5 @@ IMAGE imageFontBig "fontbig.png" NONE NONE
IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE
IMAGE imageChromeLife "life.png" NONE NONE IMAGE imageChromeLife "life.png" NONE NONE
IMAGE imageChromeLife2 "life2.png" NONE NONE IMAGE imageChromeLife2 "life2.png" NONE NONE
XGM2 stageMusic "stage.vgm"

BIN
res/stage.vgm Normal file

Binary file not shown.

View file

@ -1,26 +1,86 @@
#define BG_I 8 #define BG_I 8
// #define FADE_TOP_I BG_I + 64
// #define FADE_BOTTOM_I FADE_TOP_I + 64
#define BG_OFF 24 // zone-unique block: 64x64px ground block in sky area, only visible in zone 0
// world X=256 = tile col 32, placed in sky row block 1 (tile row 8)
#define ZONE_BLOCK_WORLD_X 256
#define ZONE_BLOCK_COL ((ZONE_BLOCK_WORLD_X / 8) % 128)
#define ZONE_BLOCK_ROW 16
bool zoneBlockVisible;
fix32 prevCamera;
#define PARALLAX_COUNT 8
fix32 parallaxAccum[PARALLAX_COUNT];
static const fix32 parallaxMul[PARALLAX_COUNT] = {
FIX32(0.8), FIX32(0.7), FIX32(0.6), FIX32(0.5), FIX32(0.4), FIX32(0.3), FIX32(0.2), FIX32(0.1)
};
s16 bgScroll[28];
void loadBackground(){ void loadBackground(){
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
VDP_setVerticalScroll(BG_B, 32);
VDP_loadTileSet(sky.tileset, BG_I, DMA); VDP_loadTileSet(sky.tileset, BG_I, DMA);
VDP_loadTileSet(ground.tileset, BG_I + 64, DMA); VDP_loadTileSet(ground.tileset, BG_I + 64, DMA);
// VDP_loadTileSet(fadeTop.tileset, FADE_TOP_I, DMA); VDP_loadTileSet(fadeBottom.tileset, BG_I + 64 + 64, DMA);
// VDP_loadTileSet(fadeBottom.tileset, FADE_BOTTOM_I, DMA); VDP_loadTileSet(fadeTop.tileset, BG_I + 64 + 64 + 4, DMA);
for(u8 y = 0; y < 4; y++){ for(u8 y = 0; y < 4; y++){
for(u8 x = 0; x < 16; x++){ for(u8 x = 0; x < 16; x++){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + (y > 2 ? 64 : 0)), x * 8, y * 8, 8, 8); VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + (y > 2 ? 64 : 0)), x * 8, y * 8, 8, 8);
} }
} }
// for(u8 x = 0; x < 5; x++){ // place 64x64 ground block in sky area (zone 0 only)
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, FADE_TOP_I), x * 8, 0, 8, 8); VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, FADE_BOTTOM_I), x * 8, 20, 8, 8); zoneBlockVisible = TRUE;
// } prevCamera = player.camera;
for(u8 i = 0; i < PARALLAX_COUNT; i++)
parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]);
// write initial scroll values so first frame has correct parallax
s16 initScroll = fix32ToInt(-player.camera);
for(u8 i = 0; i < 20; i++)
bgScroll[i] = initScroll;
for(u8 i = 0; i < 8; i++)
bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i]));
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
// fade
// for(u8 x = 0; x < 20; x++)
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 + 64), x * 2, 26, 2, 2);
// for(u8 x = 0; x < 10; x++)
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 + 64 + 4), x * 4, 22, 4, 4);
} }
void updateBackground(){ void updateBackground(){
VDP_setHorizontalScroll(BG_B, fix32ToInt(-player.camera)); s16 scrollVal = fix32ToInt(-player.camera);
VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 3);
// accumulate parallax from camera delta (not absolute position)
// this avoids discontinuities at world wrap boundaries
// GAME_WRAP is already fix32, so use directly (no FIX32 wrapper)
fix32 delta = player.camera - prevCamera;
if(delta > GAME_WRAP / 2) delta -= GAME_WRAP;
else if(delta < -(GAME_WRAP / 2)) delta += GAME_WRAP;
prevCamera = player.camera;
// update accumulators once, reuse for top and bottom
for(u8 i = 0; i < PARALLAX_COUNT; i++){
parallaxAccum[i] += fix32Mul(delta, parallaxMul[i]);
if(parallaxAccum[i] > FIX32(1024)) parallaxAccum[i] -= FIX32(1024);
else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024);
}
for(u8 i = 0; i < 20; i++)
bgScroll[i] = scrollVal;
for(u8 i = 0; i < 8; i++)
bgScroll[27 - i] = (scrollVal - fix32ToInt(parallaxAccum[i]));
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
// show ground block only when zone 0 copy of these columns is on screen
fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160));
bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212));
if(shouldShow && !zoneBlockVisible){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
zoneBlockVisible = TRUE;
} else if(!shouldShow && zoneBlockVisible){
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
zoneBlockVisible = FALSE;
}
} }

View file

@ -144,11 +144,17 @@ static void collideWithPlayer(u8 i){
fix32ToInt(deltaX), fix32ToInt(deltaX),
fix32ToInt(deltaY)); fix32ToInt(deltaY));
if(dist <= 4){ if(dist <= 4){
// convert enemy bullet to player bullet explosion in-place
SPR_setDefinition(bullets[i].image, &pBulletSprite);
bullets[i].player = TRUE;
bullets[i].pos.x = player.pos.x;
bullets[i].pos.y = player.pos.y;
killBullet(i, TRUE); killBullet(i, TRUE);
sfxExplosion(); sfxExplosion();
player.lives--; player.lives--;
if(player.lives == 0){ if(player.lives == 0){
gameOver = TRUE; gameOver = TRUE;
XGM2_stop();
} else { } else {
player.recoveringClock = 120; player.recoveringClock = 120;
killBullets = TRUE; killBullets = TRUE;

View file

@ -174,6 +174,15 @@ static void updateMap(){
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow); VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow);
} }
s16 lastLevel;
static void drawLevel(){
char lvlStr[4];
uintToStr(level + 1, lvlStr, 1);
VDP_drawText("L", 1, 26);
VDP_drawText(lvlStr, 2, 26);
lastLevel = level;
}
void loadChrome(){ void loadChrome(){
VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA); VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA);
VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA); VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA);
@ -183,6 +192,7 @@ void loadChrome(){
lastScore = 1; lastScore = 1;
drawScore(); drawScore();
drawLives(); drawLives();
drawLevel();
} }
bool didGameOver; bool didGameOver;
@ -191,7 +201,25 @@ static void doGameOver(){
didGameOver = TRUE; didGameOver = TRUE;
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1); 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 < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1); for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active){
if(humans[i].state == HUMAN_COLLECTED){
// spawn player bullet explosion at carried human position
struct bulletSpawner spawner = {
.x = humans[i].pos.x, .y = humans[i].pos.y,
.anim = 0, .speed = 0, .angle = 0, .player = TRUE
};
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
&& bullets[j].pos.x == humans[i].pos.x && bullets[j].pos.y == humans[i].pos.y){
killBullet(j, TRUE);
break;
}
}
}
killHuman(i);
}
SPR_releaseSprite(player.image); SPR_releaseSprite(player.image);
// clear minimap // clear minimap
VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H); VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H);
@ -210,6 +238,7 @@ static void showPause(){
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[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 < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1); for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1);
SPR_setPalette(player.image, PAL1); SPR_setPalette(player.image, PAL1);
XGM2_pause();
VDP_drawText("PAUSE", 17, 13); VDP_drawText("PAUSE", 17, 13);
} }
@ -218,6 +247,7 @@ static void clearPause(){
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[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 < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL0); for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL0);
SPR_setPalette(player.image, PAL0); SPR_setPalette(player.image, PAL0);
XGM2_resume();
VDP_clearText(17, 13, 5); VDP_clearText(17, 13, 5);
} }
@ -276,5 +306,6 @@ void updateChrome(){
drawScore(); drawScore();
} }
if(lastLives != player.lives) drawLives(); if(lastLives != player.lives) drawLives();
if(lastLevel != level) drawLevel();
if(clock % 4 == 0) updateMap(); if(clock % 4 == 0) updateMap();
} }

View file

@ -49,10 +49,12 @@ void spawnEnemy(u8 type, u8 zone){
enemies[i].active = FALSE; enemies[i].active = FALSE;
return; return;
} }
SPR_setVisibility(enemies[i].image, HIDDEN);
enemies[i].hp = 1; enemies[i].hp = 1;
for(u8 j = 0; j < PROP_COUNT; j++){ for(u8 j = 0; j < PROP_COUNT; j++){
enemies[i].ints[j] = 0; enemies[i].ints[j] = 0;
} }
enemies[i].ints[3] = -1;
switch(enemies[i].type){ switch(enemies[i].type){
case ENEMY_TYPE_TEST: case ENEMY_TYPE_TEST:
loadEnemyOne(i); loadEnemyOne(i);
@ -146,9 +148,31 @@ static void updateEnemy(u8 i){
break; break;
} }
// enemy->player collision
if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0){
fix32 edx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
fix32 edy = enemies[i].pos.y - player.pos.y;
if(edx >= FIX32(-16) && edx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){
sfxExplosion();
if(enemies[i].type != ENEMY_TYPE_BOSS){
enemies[i].hp = 0;
killEnemy(i);
}
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
XGM2_stop();
} else {
player.recoveringClock = 120;
killBullets = TRUE;
}
}
}
s16 sx = getScreenX(enemies[i].pos.x, player.camera); s16 sx = getScreenX(enemies[i].pos.x, player.camera);
s16 sy = fix32ToInt(enemies[i].pos.y); s16 sy = fix32ToInt(enemies[i].pos.y);
SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN); SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN);
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0);
SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off); SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off);
enemies[i].clock++; enemies[i].clock++;

View file

@ -1,4 +1,26 @@
// test enemy -- for testing out bullet stress // =============================================================================
// --- Type 0: Test / Fairy (EnemyOne) ---
// =============================================================================
// The original enemy type. A fairy that shoots 8-bullet circular bursts and
// also seeks/abducts humans (same carry behavior as Builder).
//
// Behavior:
// - When NOT carrying: drifts at speed 2, shoots 8 bullets in a circle
// every 20 frames (only when on screen). Also scans for nearby walking
// humans every 30 frames and steers toward the closest one within 256px.
// Grabs the human when within 16px.
// - When carrying: flies upward (angle 704-832, roughly up-left to up-right)
// at speed 2. Skips all shooting. boundsEnemy() handles reaching the top
// (kills human, self-destructs -- does NOT spawn a Gunner unlike Builder).
//
// ints[0] = random shot timer offset (0-59), desynchronizes shooting from
// other enemies so they don't all fire on the same frame
// ints[2] = target human index (-1 = no target)
// ints[3] = carried human index (-1 = not carrying)
//
// Speed: 2 HP: 1 Shoots: yes (8-bullet radial, every 20 frames)
// Abducts: yes (only 1 human globally at a time via humanBeingCarried flag)
// =============================================================================
void loadEnemyOne(u8 i){ void loadEnemyOne(u8 i){
enemies[i].ints[0] = random() % 60; enemies[i].ints[0] = random() % 60;
enemies[i].ints[2] = -1; // target human index enemies[i].ints[2] = -1; // target human index
@ -90,9 +112,28 @@ void updateEnemyOne(u8 i){
} }
} }
// =============================================================================
// --- Type 1: Drone --- // --- Type 1: Drone ---
// Pressure enemy. Homes toward player, simple aimed shots. // =============================================================================
// ints[0] = random shot offset, ints[1] = recalc timer // Bread-and-butter pressure enemy. Periodically recalculates heading toward
// the player and fires single aimed bullets. The main "fodder" type -- use
// high counts (8-16) to create constant movement pressure without overwhelming
// bullet density.
//
// Behavior:
// - Recalculates heading toward player every 30 frames (ints[1] counter).
// Between recalcs, travels in a straight line at speed 2.
// - Fires 1 aimed bullet at player every 40 frames. Only shoots when:
// a) on screen, AND b) level index >= 1 (i.e. L2+, so L1 drones are
// harmless). This is hardcoded, separate from LevelDef.dronesShoot.
// - Bounces off top/bottom screen edges.
//
// ints[0] = random shot timer offset (0-59), prevents synchronized volleys
// ints[1] = heading recalculation timer (counts up to 30)
//
// Speed: 2 HP: 1 Shoots: yes (1 aimed bullet, every 40 frames, L2+ only)
// Abducts: no
// =============================================================================
void loadDrone(u8 i){ void loadDrone(u8 i){
enemies[i].ints[0] = random() % 60; enemies[i].ints[0] = random() % 60;
enemies[i].ints[1] = 0; enemies[i].ints[1] = 0;
@ -132,9 +173,32 @@ void updateDrone(u8 i){
} }
} }
// =============================================================================
// --- Type 2: Gunner --- // --- Type 2: Gunner ---
// Bullet geometry. Slow drift, patterned danmaku. // =============================================================================
// ints[0] = pattern type (0=radial, 1=aimed fan), ints[1] = shot timer offset, ints[2] = angle accumulator // Danmaku / bullet geometry enemy. Drifts very slowly (speed 0.5), acting more
// like a turret than a chaser. Only fires when on screen. The bullet pattern is
// set by LevelDef.gunnerPattern (written to ints[0] by loadLevel).
//
// Behavior:
// - Drifts at speed 0.5 in a random initial direction. Bounces off top/bottom.
// - Does nothing when off screen (early return).
// - Pattern 0 (Radial Burst): fires 8 bullets in a circle every 60 frames.
// Start angle rotates by 24 each volley, creating a spiral-over-time effect.
// Bullet speed 3. Predictable, good for learning dodge patterns.
// - Pattern 1 (Aimed Fan): fires 5 bullets aimed at player, spread across
// +-64 angle units, every 45 frames. Bullet speed 3. More aggressive and
// targeted -- harder to dodge at close range.
// - Pattern is set per-gunner at level load. gunnerPattern=2 means each
// gunner randomly picks 0 or 1, creating mixed bullet fields.
//
// ints[0] = pattern type (0=radial, 1=aimed fan). Set by loadLevel, not random.
// ints[1] = random shot timer offset (0-59)
// ints[2] = angle accumulator (radial pattern only, rotates start angle)
//
// Speed: 0.5 HP: 1 Shoots: yes (pattern-dependent, see above)
// Abducts: no
// =============================================================================
void loadGunner(u8 i){ void loadGunner(u8 i){
enemies[i].ints[0] = random() % 2; enemies[i].ints[0] = random() % 2;
enemies[i].ints[1] = random() % 60; enemies[i].ints[1] = random() % 60;
@ -187,8 +251,30 @@ void updateGunner(u8 i){
} }
} }
// =============================================================================
// --- Type 3: Hunter --- // --- Type 3: Hunter ---
// Fast chaser. Homes toward player every frame. No shooting. // =============================================================================
// Fast, relentless chaser. Homes toward the player EVERY frame with speed 5
// (matching player's normal speed). Never shoots -- pure body-collision threat.
// Forces the player to keep moving and use focus mode / diagonal movement to
// outrun. Very dangerous in groups.
//
// Behavior:
// - Recalculates angle toward player every frame (no delay like Drone).
// - Moves at speed 5 (player normal speed = 6, focus = 3.5).
// - Bounces off top/bottom screen edges.
// - No shooting, no abduction. Just chases.
//
// No custom ints used.
//
// Speed: 5 HP: 1 Shoots: no Abducts: no
//
// Design notes:
// - 2-3 hunters alongside gunners is very hard (dodge bullets while fleeing)
// - 4+ hunters with no gunners is a pure movement/positioning challenge
// - 6 hunters is near-impossible -- only for late-game punishment
// - Hunters are the anti-camping enemy: you can't sit still
// =============================================================================
void loadHunter(u8 i){ void loadHunter(u8 i){
enemies[i].angle = random() % 1024; enemies[i].angle = random() % 1024;
enemies[i].speed = FIX32(5); enemies[i].speed = FIX32(5);
@ -203,9 +289,39 @@ void updateHunter(u8 i){
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed);
} }
// =============================================================================
// --- Type 4: Builder (Abductor) --- // --- Type 4: Builder (Abductor) ---
// Seeks and abducts humans. On reaching top with human, spawns a Gunner. // =============================================================================
// ints[0] = scan offset, ints[2] = target human, ints[3] = carried human // Human abductor. Drifts slowly, scans for walking humans, grabs one, and
// flies upward. If it reaches the top of the screen with a human, the human
// is killed and a Gunner spawns at that position -- punishing the player for
// not intercepting. Only 1 human can be globally carried at a time
// (humanBeingCarried flag).
//
// Behavior:
// - When NOT carrying: drifts at speed 0.7. Scans for nearest walking human
// within 256px every 30 frames. If a target is found, switches to seeking
// speed (1.4) and homes toward it. Grabs when within 16px.
// - When carrying: flies upward (angle 704-832) at speed 1.4.
// boundsEnemy() handles reaching the top: kills the human, spawns a Gunner
// at the builder's position, then self-destructs.
// - If the carried human gets collected by the player while being carried,
// boundsEnemy() detects this and kills the builder (enemy dies, human safe).
// - Cancels its target if another enemy is already carrying a human.
// - No shooting at all.
//
// ints[0] = random scan timer offset (0-59)
// ints[2] = target human index (-1 = no target)
// ints[3] = carried human index (-1 = not carrying)
//
// Speed: 0.7 (drift), 1.4 (seeking/carrying) HP: 1 Shoots: no
// Abducts: yes (kills human + spawns Gunner if it reaches the top)
//
// Design notes:
// - 1 builder adds light urgency. 2 builders is stressful.
// - Pairs well with drones to split the player's attention.
// - The Gunner spawn on success makes ignoring builders snowball badly.
// =============================================================================
void loadBuilder(u8 i){ void loadBuilder(u8 i){
enemies[i].ints[0] = random() % 60; enemies[i].ints[0] = random() % 60;
enemies[i].ints[2] = -1; enemies[i].ints[2] = -1;
@ -274,62 +390,243 @@ void updateBuilder(u8 i){
} }
} }
// =============================================================================
// --- Type 5: Boss --- // --- Type 5: Boss ---
// High HP, alternates 2 patterns. hp set by level data via ints[0]. // =============================================================================
// ints[0] = initial hp (set by stage), ints[1] = pattern timer, ints[2] = current pattern // High-HP enemy with phase-based attack patterns. Boss number is set via
// pendingBossNum global before spawn (because spawnEnemy zeroes all ints[]).
// HP is set via pendingBossHp. Both are auto-configured by loadLevel() based
// on the level's bossHp field and level index.
//
// Behavior:
// - Speed 1, bounces off top/bottom. Only attacks when on screen.
// - Attack pattern changes based on remaining HP. HP range is divided into
// N equal phases. Phase 0 = full health, highest phase = near death.
// Later bosses have more phases and higher HP, so they cycle through
// more varied and increasingly aggressive patterns.
// - Each boss (1-5) has a unique updateBoss function with different pattern
// selections and timings per phase.
//
// ints[0] = boss number (0-4), selects which updateBoss variant runs
// ints[4] = max HP (stored at load for phase calculation)
//
// Boss 1 (L4): 25 HP, 2 phases -- radial, then aimed fan
// Boss 2 (L8): 50 HP, 3 phases -- radial, aimed fan, spiral
// Boss 3 (L12): 75 HP, 4 phases -- radial, spiral, aimed fan, double radial
// Boss 4 (L16): 100 HP, 5 phases -- adds wide spray
// Boss 5 (L20): 125 HP, 6 phases -- adds ring burst (all patterns used)
//
// Available boss attack patterns:
// Radial: N bullets in even circle, random start angle. Predictable.
// Aimed Fan: N bullets in arc aimed at player. Targeted.
// Spiral: N bullets in circle, start angle rotates with clock. Mesmerizing.
// Double Radial: Two offset rings (outer fast, inner slow). Dense.
// Wide Spray: N bullets in random spread toward player. Chaotic.
// Ring Burst: N bullets in even circle (like radial but different anim). Climactic.
//
// Speed: 1 HP: varies Shoots: yes (phase-dependent) Abducts: no
// =============================================================================
// shared pattern functions
static void bossPatternRadial(u8 i, u8 count, fix32 speed){
sfxEnemyShotB();
s16 baseAngle = random() % 1024;
struct bulletSpawner spawner = {
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
.anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle,
};
s16 step = 1024 / count;
for(u8 j = 0; j < count; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += step;
}
}
static void bossPatternAimedFan(u8 i, u8 count, s16 spread, fix32 speed){
sfxEnemyShotA();
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
fix32 dy = player.pos.y - enemies[i].pos.y;
s16 aimAngle = honeAngle(
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
s16 step = (count > 1) ? (spread * 2) / (count - 1) : 0;
struct bulletSpawner spawner = {
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
.anim = 9 + (random() % 3), .speed = speed, .angle = aimAngle - spread,
};
for(u8 j = 0; j < count; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += step;
}
}
static void bossPatternSpiral(u8 i, u8 count, fix32 speed){
sfxEnemyShotC();
s16 baseAngle = (enemies[i].clock * 17) % 1024;
struct bulletSpawner spawner = {
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
.anim = 6 + (random() % 3), .speed = speed, .angle = baseAngle,
};
s16 step = 1024 / count;
for(u8 j = 0; j < count; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += step;
}
}
static void bossPatternDoubleRadial(u8 i, u8 count, fix32 speed){
sfxEnemyShotB();
s16 baseAngle = random() % 1024;
s16 step = 1024 / count;
struct bulletSpawner spawner = {
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
.anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle,
};
for(u8 j = 0; j < count; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += step;
}
spawner.angle = baseAngle + step / 2;
spawner.speed = speed - FIX32(1);
spawner.anim = 6 + (random() % 3);
for(u8 j = 0; j < count; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += step;
}
}
static void bossPatternWideSpray(u8 i, u8 count, fix32 speed){
sfxEnemyShotA();
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
fix32 dy = player.pos.y - enemies[i].pos.y;
s16 aimAngle = honeAngle(
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
struct bulletSpawner spawner = {
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
.anim = 3 + (random() % 9), .speed = speed, .angle = aimAngle,
};
for(u8 j = 0; j < count; j++){
spawner.angle = aimAngle - 128 + (random() % 256);
spawner.speed = speed - FIX32(random() % 2);
spawnBullet(spawner, EMPTY);
}
}
static void bossPatternRingBurst(u8 i, u8 count, fix32 speed){
sfxEnemyShotB();
sfxEnemyShotC();
s16 baseAngle = random() % 1024;
s16 step = 1024 / count;
struct bulletSpawner spawner = {
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
.anim = 9 + (random() % 3), .speed = speed, .angle = baseAngle,
};
for(u8 j = 0; j < count; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += step;
}
}
// get current phase based on HP. phase 0 = full health, higher = more damaged
static u8 getBossPhase(u8 i, u8 numPhases){
s16 maxHp = enemies[i].ints[4];
if(maxHp <= 0) return 0;
s16 lost = maxHp - enemies[i].hp;
if(lost < 0) lost = 0;
u8 phase = (lost * numPhases) / maxHp;
if(phase >= numPhases) phase = numPhases - 1;
return phase;
}
void loadBoss(u8 i){ void loadBoss(u8 i){
enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 10; enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 25;
pendingBossHp = 0; pendingBossHp = 0;
enemies[i].ints[0] = pendingBossNum;
pendingBossNum = 0;
enemies[i].ints[1] = 0; enemies[i].ints[1] = 0;
enemies[i].ints[2] = 0; enemies[i].ints[4] = enemies[i].hp;
enemies[i].angle = random() % 1024; enemies[i].angle = random() % 1024;
enemies[i].speed = FIX32(1); enemies[i].speed = FIX32(1);
} }
void updateBoss(u8 i){
if(!enemies[i].onScreen) return; // Boss 1 (L6): 2 patterns, 25 HP
enemies[i].ints[1]++; static void updateBossOne(u8 i){
// alternate patterns every 180 frames u8 phase = getBossPhase(i, 2);
if(enemies[i].ints[1] >= 180){ if(phase == 0){
enemies[i].ints[1] = 0; if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 10, FIX32(3));
enemies[i].ints[2] = 1 - enemies[i].ints[2];
}
if(enemies[i].ints[2] == 0){
// Pattern A: Radial burst - 12 bullets every 50 frames
if(enemies[i].ints[1] % 50 == 0){
sfxEnemyShotB();
s16 baseAngle = random() % 1024;
struct bulletSpawner spawner = {
.x = enemies[i].pos.x,
.y = enemies[i].pos.y,
.anim = 3 + (random() % 3),
.speed = FIX32(3),
.angle = baseAngle,
};
for(u8 j = 0; j < 12; j++){
spawnBullet(spawner, EMPTY);
spawner.angle += 85;
}
}
} else { } else {
// Pattern B: Aimed wide fan - 8 bullets every 40 frames if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 6, 80, FIX32(3));
if(enemies[i].ints[1] % 40 == 0){ }
sfxEnemyShotA(); }
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
fix32 dy = player.pos.y - enemies[i].pos.y; // Boss 2 (L12): 3 patterns, 50 HP
s16 aimAngle = honeAngle( static void updateBossTwo(u8 i){
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), u8 phase = getBossPhase(i, 3);
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); if(phase == 0){
struct bulletSpawner spawner = { if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3));
.x = enemies[i].pos.x, } else if(phase == 1){
.y = enemies[i].pos.y, if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3));
.anim = 9 + (random() % 3), } else {
.speed = FIX32(3), if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 6, FIX32(4));
.angle = aimAngle - 112, }
}; }
for(u8 j = 0; j < 8; j++){
spawnBullet(spawner, EMPTY); // Boss 3 (L18): 4 patterns, 75 HP
spawner.angle += 32; static void updateBossThree(u8 i){
} u8 phase = getBossPhase(i, 4);
} if(phase == 0){
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3));
} else if(phase == 1){
if(enemies[i].clock % 40 == 0) bossPatternSpiral(i, 8, FIX32(3));
} else if(phase == 2){
if(enemies[i].clock % 35 == 0) bossPatternAimedFan(i, 10, 112, FIX32(4));
} else {
if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 8, FIX32(4));
}
}
// Boss 4 (L24): 5 patterns, 100 HP
static void updateBossFour(u8 i){
u8 phase = getBossPhase(i, 5);
if(phase == 0){
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 14, FIX32(3));
} else if(phase == 1){
if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3));
} else if(phase == 2){
if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 8, FIX32(4));
} else if(phase == 3){
if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4));
} else {
if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 12, FIX32(4));
}
}
// Boss 5 (L30): 6 patterns, 125 HP
static void updateBossFive(u8 i){
u8 phase = getBossPhase(i, 6);
if(phase == 0){
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 16, FIX32(3));
} else if(phase == 1){
if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 10, 112, FIX32(3));
} else if(phase == 2){
if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 10, FIX32(4));
} else if(phase == 3){
if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4));
} else if(phase == 4){
if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 14, FIX32(5));
} else {
if(enemies[i].clock % 20 == 0) bossPatternRingBurst(i, 16, FIX32(5));
}
}
void updateBoss(u8 i){
if(!enemies[i].onScreen) return;
switch(enemies[i].ints[0]){
case 0: updateBossOne(i); break;
case 1: updateBossTwo(i); break;
case 2: updateBossThree(i); break;
case 3: updateBossFour(i); break;
case 4: updateBossFive(i); break;
} }
} }

View file

@ -10,6 +10,9 @@ u32 clock;
#define CULL_LIMIT FIX32(240) #define CULL_LIMIT FIX32(240)
// #define MUSIC_VOLUME 50
#define MUSIC_VOLUME 0
u32 score; u32 score;
#define SCORE_LENGTH 8 #define SCORE_LENGTH 8
@ -29,6 +32,7 @@ bool paused, isPausing;
s16 enemyCount, bulletCount; s16 enemyCount, bulletCount;
u8 level; u8 level;
s16 pendingBossHp; s16 pendingBossHp;
s16 pendingBossNum;
bool waitForRelease; bool waitForRelease;
bool levelClearing; bool levelClearing;
u32 levelClearClock; u32 levelClearClock;

View file

@ -25,6 +25,7 @@ void spawnHuman(u8 zone){
humans[i].active = FALSE; humans[i].active = FALSE;
return; return;
} }
SPR_setVisibility(humans[i].image, HIDDEN);
} }
static void updateHuman(u8 i){ static void updateHuman(u8 i){
@ -116,9 +117,16 @@ static void updateHuman(u8 i){
s16 sx = getScreenX(humans[i].pos.x, player.camera); s16 sx = getScreenX(humans[i].pos.x, player.camera);
s16 sy = fix32ToInt(humans[i].pos.y); s16 sy = fix32ToInt(humans[i].pos.y);
bool visible = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); bool visible = (humans[i].state == HUMAN_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN); SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN);
SPR_setPosition(humans[i].image, sx - HUMAN_OFF, sy - HUMAN_OFF); SPR_setPosition(humans[i].image, sx - HUMAN_OFF, sy - HUMAN_OFF);
// manually animate flash only when walking or falling
if(humans[i].state == HUMAN_WALKING || humans[i].state == HUMAN_FALLING)
SPR_setFrame(humans[i].image, (clock / 15) & 1);
else if(humans[i].state == HUMAN_COLLECTED)
SPR_setAnim(humans[i].image, 1);
else
SPR_setFrame(humans[i].image, 0);
} }
void updateHumans(){ void updateHumans(){

View file

@ -43,6 +43,11 @@ void loadGame(){
loadPlayer(); loadPlayer();
loadChrome(); loadChrome();
loadLevel(0); loadLevel(0);
XGM2_play(stageMusic);
XGM2_setFMVolume(MUSIC_VOLUME);
XGM2_setPSGVolume(MUSIC_VOLUME);
player.recoveringClock = 120;
killBullets = TRUE;
started = TRUE; started = TRUE;
} }
@ -59,25 +64,30 @@ static void updateGame(){
loadBackground(); loadBackground();
loadChrome(); loadChrome();
loadLevel(level + 1); loadLevel(level + 1);
XGM2_play(stageMusic);
SPR_setVisibility(player.image, VISIBLE); SPR_setVisibility(player.image, VISIBLE);
player.recoveringClock = 120;
killBullets = TRUE;
} }
return; return;
} }
if(!paused){ if(!paused){
updatePlayer(); updatePlayer();
updateBackground();
if(clock % 2 == 0){ if(clock % 2 == 0){
updateEnemies(); updateEnemies();
if(!gameOver && enemyCount == 0){ if(!gameOver && enemyCount == 0){
if(level >= LEVEL_COUNT - 1){ if(level >= LEVEL_COUNT - 1){
gameOver = TRUE; gameOver = TRUE;
XGM2_stop();
} else { } else {
levelClearing = TRUE; levelClearing = TRUE;
levelClearClock = 0; levelClearClock = 0;
XGM2_stop();
} }
} }
updateHumans(); updateHumans();
} else { } else {
updateBackground();
updateBullets(); updateBullets();
} }
} }

View file

@ -1,9 +1,9 @@
#define PLAYER_SPEED FIX32(5) #define PLAYER_SPEED FIX32(6)
#define PLAYER_SPEED_FOCUS FIX32(3) #define PLAYER_SPEED_FOCUS FIX32(3.5)
#define PLAYER_ACCEL PLAYER_SPEED >> 3 #define PLAYER_ACCEL PLAYER_SPEED >> 4
#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 3 #define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 4
#define PLAYER_OFF 24 #define PLAYER_OFF 24
#define PLAYER_BOUND_Y FIX32(PLAYER_OFF) #define PLAYER_BOUND_Y FIX32(PLAYER_OFF)

View file

@ -1,55 +1,137 @@
// =============================================================================
// LEVEL DESIGN GUIDE
// =============================================================================
//
// Each level is a single struct defining what spawns. The level ends when all
// enemies are killed (enemyCount == 0). Humans are bonus -- they don't affect
// level completion.
//
// --- STRUCT FIELDS ---
//
// drones Number of Drone enemies (type 1). Bulk pressure enemy.
// Speed 2, homes toward player every 30 frames.
// Fires 1 aimed bullet every 40 frames (only on L2+, i.e. index >= 1).
// Use dronesShoot=FALSE on L1 to introduce them without bullets.
// Good range: 6-16. Above 14 gets chaotic.
//
// gunners Number of Gunner enemies (type 2). Danmaku / bullet geometry.
// Speed 0.5 (slow drift), only shoots when on screen.
// Pattern controlled by gunnerPattern field (see below).
// Good range: 0-6. Even 2-3 gunners create significant bullet density.
//
// hunters Number of Hunter enemies (type 3). Fast chaser, no shooting.
// Speed 5 (matches player!). Homes every frame. Pure body pressure.
// Very dangerous -- 2-3 is oppressive, 6 is near-impossible.
// Good range: 0-6. Introduce after players learn movement.
//
// builders Number of Builder enemies (type 4). Human abductor.
// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking humans
// and flies upward. If it reaches the top, the human dies and a
// Gunner spawns in its place. Only 1 human can be carried at a time.
// Creates urgency -- player must choose between killing enemies
// and saving humans. Good range: 0-2.
//
// bossHp If > 0, spawns a Boss enemy (type 5) with this many HP.
// Boss number is auto-calculated from level index (lvl / 4).
// Set to 0 for non-boss levels. Boss speed is 1, bounces around.
// Boss has multiple attack phases based on remaining HP.
// Typical values: 25, 50, 75, 100, 125.
// Other enemies can coexist with the boss (adds pressure).
//
// humans Number of humans (koakuma) to spawn. Distributed across 4 zones
// (2 per zone if >= 4 humans, then 1 each for remainder).
// Walk along the ground, can be collected by player for 1000 pts
// (2000 if caught mid-fall after enemy drops them).
// Max 8 (HUMAN_COUNT). Usually just set to 8.
//
// gunnerPattern Controls what bullet pattern gunners use:
// 0 = Radial Burst: 8 bullets in a circle every 60 frames.
// Rotating start angle. Steady, predictable pressure.
// 1 = Aimed Fan: 5 bullets aimed at player, spread +-64,
// every 45 frames. More targeted/aggressive.
// 2 = Mix: each gunner randomly picks 0 or 1 at spawn.
// Creates varied, less predictable bullet fields.
//
// dronesShoot TRUE = drones fire aimed bullets (normal behavior on L2+).
// FALSE = drones still home toward player but never shoot.
// Only meaningful for the very first level as a gentle intro.
// (Drone shooting is also gated by level >= 1 in code, so
// L1 drones never shoot regardless of this flag.)
//
// --- LIMITS ---
//
// Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss
// must not exceed 24. If it does, excess enemies silently fail to spawn.
//
// Total humans: 8 slots (HUMAN_COUNT).
//
// Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets
// get priority and evict enemy bullets when full.
//
// --- SPAWNING ---
//
// Enemies are distributed across 4 zones (each 512px of the 2048px world).
// Enemy i spawns in zone (i % 4). They never spawn within 240px of the player
// and maintain 64px minimum spacing from each other.
//
// Boss always spawns in zone 1.
//
// --- DESIGN TIPS ---
//
// - Drone-heavy levels (12-16) create constant movement pressure
// - Gunner-heavy levels (4-6) create bullet reading / dodging challenges
// - Hunter levels force the player to keep moving (anti-camping)
// - Builder levels force tough choices: kill builders or save humans?
// - Combining hunters + gunners is very hard (dodge bullets while fleeing)
// - Boss levels with escort enemies (drones/gunners alongside boss) are
// harder than solo boss fights
// - A "farm" level (lots of drones, no gunners) gives score-building breathers
// - gunnerPattern 0 (radial) is easier to dodge than 1 (aimed fan)
//
// =============================================================================
struct LevelDef { struct LevelDef {
u8 drones, gunners, hunters, builders; u8 drones, gunners, hunters, builders;
u8 bossHp; u8 bossHp;
u8 humans; u8 humans;
u8 gunnerPattern; // 0=radial, 1=aimed fan, 2=mix u8 gunnerPattern;
bool dronesShoot; bool dronesShoot;
}; };
// dr gn hn bl boss hum pat shoot // dr gn hn bl boss hum pat shoot
const struct LevelDef levels[30] = { const struct LevelDef levels[20] = {
// Phase 1: "Immediate danger" (L1-L6) // Phase 1: "Immediate danger" (L1-L4)
{ 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1 { 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1
{ 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2 { 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2
{ 12, 2, 0, 0, 0, 8, 0, TRUE }, // L3 { 12, 3, 0, 0, 0, 8, 1, TRUE }, // L3
{ 10, 3, 0, 0, 0, 8, 1, TRUE }, // L4 { 8, 0, 0, 0, 25, 8, 0, TRUE }, // L4 BOSS 1
{ 14, 3, 0, 0, 0, 8, 1, TRUE }, // L5
{ 8, 0, 0, 0, 8, 8, 0, TRUE }, // L6 BOSS
// Phase 2: "You can't save everything" (L7-L12) // Phase 2: "You can't save everything" (L5-L8)
{ 10, 0, 0, 1, 0, 8, 0, TRUE }, // L7 { 10, 2, 0, 1, 0, 8, 0, TRUE }, // L5
{ 10, 2, 0, 1, 0, 8, 0, TRUE }, // L8 { 14, 3, 0, 1, 0, 8, 1, TRUE }, // L6
{ 12, 0, 0, 2, 0, 8, 0, TRUE }, // L9 { 10, 2, 0, 2, 0, 8, 2, TRUE }, // L7
{ 14, 3, 0, 1, 0, 8, 1, TRUE }, // L10 WALL { 8, 0, 0, 1, 50, 8, 0, TRUE }, // L8 BOSS 2
{ 10, 2, 0, 2, 0, 8, 2, TRUE }, // L11
{ 8, 0, 0, 1, 12, 8, 0, TRUE }, // L12 BOSS
// Phase 3: "Geometry matters" (L13-L18) // Phase 3: "Geometry matters" (L9-L12)
{ 8, 0, 4, 0, 0, 8, 0, TRUE }, // L13 { 8, 3, 4, 0, 0, 8, 1, TRUE }, // L9
{ 8, 3, 2, 0, 0, 8, 1, TRUE }, // L14 { 10, 2, 4, 0, 0, 8, 2, TRUE }, // L10
{ 16, 0, 0, 0, 0, 8, 0, TRUE }, // L15 FARM { 12, 3, 3, 0, 0, 8, 1, TRUE }, // L11
{ 10, 2, 4, 0, 0, 8, 2, TRUE }, // L16 { 0, 2, 2, 0, 75, 8, 2, TRUE }, // L12 BOSS 3
{ 12, 3, 3, 0, 0, 8, 1, TRUE }, // L17
{ 0, 2, 2, 0, 15, 8, 2, TRUE }, // L18 BOSS
// Phase 4: "Suffocation" (L19-L24) // Phase 4: "Suffocation" (L13-L16)
{ 12, 4, 0, 0, 0, 8, 2, TRUE }, // L19 { 14, 4, 0, 2, 0, 8, 2, TRUE }, // L13
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L20 WALL { 10, 0, 6, 0, 0, 8, 0, TRUE }, // L14
{ 10, 0, 6, 0, 0, 8, 0, TRUE }, // L21 { 12, 4, 2, 0, 0, 8, 1, TRUE }, // L15
{ 12, 4, 2, 0, 0, 8, 1, TRUE }, // L22 { 0, 3, 0, 1, 100, 8, 2, TRUE }, // L16 BOSS 4
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L23
{ 0, 3, 0, 1, 20, 8, 2, TRUE }, // L24 BOSS
// Phase 5: "Arcade cruelty" (L25-L30) // Phase 5: "Arcade cruelty" (L17-L20)
{ 16, 0, 4, 0, 0, 8, 0, TRUE }, // L25 { 16, 0, 4, 0, 0, 8, 0, TRUE }, // L17
{ 12, 6, 0, 0, 0, 8, 2, TRUE }, // L26 { 14, 4, 4, 2, 0, 8, 2, TRUE }, // L18
{ 14, 2, 4, 0, 0, 8, 1, TRUE }, // L27 { 6, 2, 2, 1, 50, 8, 2, TRUE }, // L19 MINI-BOSS
{ 16, 4, 0, 2, 0, 8, 2, TRUE }, // L28 { 4, 2, 2, 1, 125, 8, 2, TRUE }, // L20 BOSS 5 FINAL
{ 6, 2, 2, 1, 10, 8, 2, TRUE }, // L29 MINI-BOSS
{ 4, 2, 2, 1, 30, 8, 2, TRUE }, // L30 FINAL
}; };
#define LEVEL_COUNT 30 #define LEVEL_COUNT 20
static void distributeEnemies(u8 type, u8 count){ static void distributeEnemies(u8 type, u8 count){
for(u8 i = 0; i < count; i++){ for(u8 i = 0; i < count; i++){
@ -80,6 +162,9 @@ void loadLevel(u8 lvl){
if(def->bossHp > 0){ if(def->bossHp > 0){
pendingBossHp = def->bossHp; pendingBossHp = def->bossHp;
pendingBossNum = lvl / 4; // L3=0, L7=1, L11=2, L15=3, L18+=4
if(pendingBossNum > 4) pendingBossNum = 4;
if(lvl == 18) pendingBossNum = 1; // L19 mini-boss reuses boss 2
spawnEnemy(ENEMY_TYPE_BOSS, 1); spawnEnemy(ENEMY_TYPE_BOSS, 1);
} }