diff --git a/flower.fur b/flower.fur new file mode 100644 index 0000000..84c0ab4 Binary files /dev/null and b/flower.fur differ diff --git a/res/human.png b/res/human.png index 10d026d..619fdd3 100644 Binary files a/res/human.png and b/res/human.png differ diff --git a/res/koakuma.png b/res/koakuma.png new file mode 100644 index 0000000..c930e29 Binary files /dev/null and b/res/koakuma.png differ diff --git a/res/resources.res b/res/resources.res index 588ee61..a734309 100644 --- a/res/resources.res +++ b/res/resources.res @@ -21,6 +21,7 @@ SPRITE butterflySprite "butterfly.png" 4 4 NONE 8 SPRITE fairySprite "fairy2.png" 4 4 NONE 8 SPRITE aliceSprite "alice.png" 6 6 NONE 0 SPRITE humanSprite "human.png" 2 2 NONE 0 +SPRITE koaSprite "koakuma.png" 4 4 NONE 0 // IMAGE mapPlayer "mapplayer.png" NONE NONE IMAGE mapIndicator "mapindicator.png" NONE NONE diff --git a/src/chrome.h b/src/chrome.h index b3ec46c..d17a457 100644 --- a/src/chrome.h +++ b/src/chrome.h @@ -258,6 +258,19 @@ void updateChrome(){ SYS_hardReset(); return; } + // level transition overlay + if(levelClearing){ + if(levelClearClock == 2){ + char lvlStr[4]; + uintToStr(level + 2, lvlStr, 1); + VDP_drawText("LEVEL ", 15, 13); + VDP_drawText(lvlStr, 21, 13); + } + if(levelClearClock >= 110){ + VDP_clearText(15, 13, 10); + } + return; + } if(lastScore != score){ lastScore = score; drawScore(); diff --git a/src/enemies.h b/src/enemies.h index 1050fca..d7b6936 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -29,14 +29,16 @@ void spawnEnemy(u8 type, u8 zone){ // Calculate zone bounds (each zone is 512px) fix32 zoneStart = FIX32(zone * 512); - fix32 randX, randY; + fix32 randX, randY, playerDist; u16 attempts = 0; do { // Random X within zone: zoneStart + random(0-511) randX = zoneStart + FIX32(random() % 512); randY = FIX32(16 + (random() % 128)); attempts++; - } while(!isValidEnemyPosition(randX, randY) && attempts < 100); + playerDist = getWrappedDelta(randX, player.pos.x); + if(playerDist < 0) playerDist = -playerDist; + } while((playerDist < CULL_LIMIT || !isValidEnemyPosition(randX, randY)) && attempts < 100); enemies[i].pos.x = randX; enemies[i].pos.y = randY; @@ -47,13 +49,29 @@ void spawnEnemy(u8 type, u8 zone){ enemies[i].active = FALSE; return; } + enemies[i].hp = 1; for(u8 j = 0; j < PROP_COUNT; j++){ enemies[i].ints[j] = 0; } switch(enemies[i].type){ - case 0: + case ENEMY_TYPE_TEST: loadEnemyOne(i); break; + case ENEMY_TYPE_DRONE: + loadDrone(i); + break; + case ENEMY_TYPE_GUNNER: + loadGunner(i); + break; + case ENEMY_TYPE_HUNTER: + loadHunter(i); + break; + case ENEMY_TYPE_BUILDER: + loadBuilder(i); + break; + case ENEMY_TYPE_BOSS: + loadBoss(i); + break; } enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); @@ -61,7 +79,7 @@ void spawnEnemy(u8 type, u8 zone){ } static void boundsEnemy(u8 i){ - if(enemies[i].ints[3] >= 0){ + 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 the human was collected by player or gone, kill this enemy if(!humans[h].active || humans[h].state == HUMAN_COLLECTED){ @@ -75,7 +93,11 @@ static void boundsEnemy(u8 i){ if(humans[h].active) killHuman(h); enemies[i].ints[3] = -1; humanBeingCarried = FALSE; - // TODO: spawn mutant here + if(enemies[i].type == ENEMY_TYPE_BUILDER){ + u8 zone = fix32ToInt(enemies[i].pos.x) / 512; + spawnEnemy(ENEMY_TYPE_GUNNER, zone); + } + enemies[i].hp = 0; killEnemy(i); return; } @@ -104,9 +126,24 @@ static void updateEnemy(u8 i){ enemies[i].onScreen = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); switch(enemies[i].type){ - case 0: + case ENEMY_TYPE_TEST: updateEnemyOne(i); break; + case ENEMY_TYPE_DRONE: + updateDrone(i); + break; + case ENEMY_TYPE_GUNNER: + updateGunner(i); + break; + case ENEMY_TYPE_HUNTER: + updateHunter(i); + break; + case ENEMY_TYPE_BUILDER: + updateBuilder(i); + break; + case ENEMY_TYPE_BOSS: + updateBoss(i); + break; } s16 sx = getScreenX(enemies[i].pos.x, player.camera); diff --git a/src/enemytypes.h b/src/enemytypes.h index f107ff3..588d0b0 100644 --- a/src/enemytypes.h +++ b/src/enemytypes.h @@ -1,3 +1,4 @@ +// test enemy -- for testing out bullet stress void loadEnemyOne(u8 i){ enemies[i].ints[0] = random() % 60; enemies[i].ints[2] = -1; // target human index @@ -5,7 +6,6 @@ void loadEnemyOne(u8 i){ enemies[i].angle = ((random() % 4) * 256) + 128; enemies[i].speed = FIX32(2); } - void updateEnemyOne(u8 i){ // carrying behavior: move upward, skip shooting if(enemies[i].ints[3] >= 0){ @@ -89,3 +89,247 @@ void updateEnemyOne(u8 i){ } } } + +// --- Type 1: Drone --- +// Pressure enemy. Homes toward player, simple aimed shots. +// ints[0] = random shot offset, ints[1] = recalc timer +void loadDrone(u8 i){ + enemies[i].ints[0] = random() % 60; + enemies[i].ints[1] = 0; + enemies[i].angle = random() % 1024; + enemies[i].speed = FIX32(2); +} +void updateDrone(u8 i){ + // recalculate heading toward player every 30 frames + enemies[i].ints[1]++; + if(enemies[i].ints[1] >= 30){ + enemies[i].ints[1] = 0; + fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); + fix32 dy = player.pos.y - enemies[i].pos.y; + enemies[i].angle = 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)); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + } + // shooting: 1 aimed bullet every ~40 frames (only if level >= 1 i.e. L2+) + if(level >= 1 && enemies[i].onScreen && + enemies[i].clock % 40 == (u32)(enemies[i].ints[0]) % 40){ + sfxEnemyShotC(); + 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 = FIX32(4), + .angle = aimAngle, + }; + spawnBullet(spawner, EMPTY); + } +} + +// --- 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 +void loadGunner(u8 i){ + enemies[i].ints[0] = random() % 2; + enemies[i].ints[1] = random() % 60; + enemies[i].ints[2] = 0; + enemies[i].angle = random() % 1024; + enemies[i].speed = FIX32(0.5); +} +void updateGunner(u8 i){ + if(!enemies[i].onScreen) return; + if(enemies[i].ints[0] == 0){ + // Pattern 0: Radial Burst - 8 bullets every ~60 frames + if(enemies[i].clock % 60 == (u32)(enemies[i].ints[1]) % 60){ + sfxEnemyShotB(); + s16 baseAngle = enemies[i].ints[2]; + 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 < 8; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += 128; + } + enemies[i].ints[2] += 24; + if(enemies[i].ints[2] >= 1024) enemies[i].ints[2] -= 1024; + } + } else { + // Pattern 1: Aimed Fan - 5 bullets spread +-64 every ~45 frames + if(enemies[i].clock % 45 == (u32)(enemies[i].ints[1]) % 45){ + 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 = 6 + (random() % 3), + .speed = FIX32(3), + .angle = aimAngle - 64, + }; + for(u8 j = 0; j < 5; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += 32; + } + } + } +} + +// --- Type 3: Hunter --- +// Fast chaser. Homes toward player every frame. No shooting. +void loadHunter(u8 i){ + enemies[i].angle = random() % 1024; + enemies[i].speed = FIX32(5); +} +void updateHunter(u8 i){ + fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); + fix32 dy = player.pos.y - enemies[i].pos.y; + enemies[i].angle = 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)); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); +} + +// --- 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 +void loadBuilder(u8 i){ + enemies[i].ints[0] = random() % 60; + enemies[i].ints[2] = -1; + enemies[i].ints[3] = -1; + enemies[i].angle = random() % 1024; + enemies[i].speed = FIX32(0.7); +} +void updateBuilder(u8 i){ + // carrying: steer upward + if(enemies[i].ints[3] >= 0){ + enemies[i].angle = 704 + (random() % 128); + enemies[i].speed = FIX32(1.4); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + return; + } + + // cancel target if a human is already being carried + if(humanBeingCarried && enemies[i].ints[2] >= 0){ + enemies[i].ints[2] = -1; + } + + // scan for nearest walking human every 30 frames + if(!humanBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){ + s16 bestHuman = -1; + fix32 bestDist = FIX32(9999); + for(s16 j = 0; j < HUMAN_COUNT; j++){ + if(!humans[j].active || humans[j].state != HUMAN_WALKING) continue; + fix32 dx = getWrappedDelta(enemies[i].pos.x, humans[j].pos.x); + fix32 dy = enemies[i].pos.y - humans[j].pos.y; + fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy); + if(dist < bestDist && dist < FIX32(256)){ + bestDist = dist; + bestHuman = j; + } + } + enemies[i].ints[2] = bestHuman; + } + + // steer toward target human + if(enemies[i].ints[2] >= 0){ + s16 t = enemies[i].ints[2]; + if(!humans[t].active || humans[t].state != HUMAN_WALKING){ + enemies[i].ints[2] = -1; + } else { + fix32 dx = getWrappedDelta(humans[t].pos.x, enemies[i].pos.x); + fix32 dy = humans[t].pos.y - enemies[i].pos.y; + enemies[i].speed = FIX32(1.4); + s16 angle = honeAngle( + fix32ToFix16(enemies[i].pos.x), fix32ToFix16(humans[t].pos.x), + fix32ToFix16(enemies[i].pos.y), fix32ToFix16(humans[t].pos.y)); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed); + + // grab check + fix32 adx = dx < 0 ? -dx : dx; + fix32 ady = dy < 0 ? -dy : dy; + if(adx < FIX32(16) && ady < FIX32(16)){ + enemies[i].ints[3] = t; + enemies[i].ints[2] = -1; + humanBeingCarried = TRUE; + humans[t].state = HUMAN_CARRIED; + humans[t].carriedBy = i; + } + } + } +} + +// --- 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 +void loadBoss(u8 i){ + enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 10; + pendingBossHp = 0; + enemies[i].ints[1] = 0; + enemies[i].ints[2] = 0; + enemies[i].angle = random() % 1024; + enemies[i].speed = FIX32(1); +} +void updateBoss(u8 i){ + if(!enemies[i].onScreen) return; + enemies[i].ints[1]++; + // alternate patterns every 180 frames + if(enemies[i].ints[1] >= 180){ + enemies[i].ints[1] = 0; + 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 { + // Pattern B: Aimed wide fan - 8 bullets every 40 frames + 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; + 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 = 9 + (random() % 3), + .speed = FIX32(3), + .angle = aimAngle - 112, + }; + for(u8 j = 0; j < 8; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += 32; + } + } + } +} \ No newline at end of file diff --git a/src/global.h b/src/global.h index a323e45..f64d9a4 100644 --- a/src/global.h +++ b/src/global.h @@ -1,6 +1,6 @@ u32 clock; #define CLOCK_LIMIT 32000 -#define PROP_COUNT 4 +#define PROP_COUNT 8 #define GAME_H_F FIX32(224) @@ -27,6 +27,11 @@ bool started; bool gameOver; bool paused, isPausing; s16 enemyCount, bulletCount; +u8 level; +s16 pendingBossHp; +bool waitForRelease; +bool levelClearing; +u32 levelClearClock; // controls struct controls { @@ -81,11 +86,19 @@ struct bullet bullets[BULLET_COUNT]; // enemies -#define ENEMY_COUNT 16 +#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 struct enemy { bool active, onScreen; u8 type; + s16 hp; s16 angle, off; u32 clock; fix32 speed; @@ -151,6 +164,8 @@ void killBullet(u8 i, bool explode){ } void killEnemy(u8 i){ + enemies[i].hp--; + if(enemies[i].hp > 0) return; if(enemies[i].ints[3] >= 0){ s16 h = enemies[i].ints[3]; if(humans[h].active){ diff --git a/src/humans.h b/src/humans.h index 49db304..db3d57a 100644 --- a/src/humans.h +++ b/src/humans.h @@ -105,7 +105,7 @@ static void updateHuman(u8 i){ fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x); if(humans[i].state != HUMAN_CARRIED && humans[i].state != HUMAN_COLLECTED){ fix32 dy = humans[i].pos.y - player.pos.y; - if(dx >= FIX32(-24) && dx <= FIX32(24) && dy >= FIX32(-24) && dy <= FIX32(24)){ + if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){ score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000; sfxPickup(); humans[i].state = HUMAN_COLLECTED; diff --git a/src/main.c b/src/main.c index bfdbaa3..9d62b8d 100644 --- a/src/main.c +++ b/src/main.c @@ -23,22 +23,58 @@ static void loadInternals(){ VDP_setTextPriority(1); } +void clearLevel(){ + for(s16 i = 0; i < BULLET_COUNT; i++) + if(bullets[i].active) killBullet(i, FALSE); + for(s16 i = 0; i < ENEMY_COUNT; i++) + if(enemies[i].active){ enemies[i].hp = 0; killEnemy(i); } + for(s16 i = 0; i < HUMAN_COUNT; i++) + if(humans[i].active) killHuman(i); + humanBeingCarried = FALSE; + collectedCount = 0; + // black out everything + SPR_setVisibility(player.image, HIDDEN); + VDP_clearTileMapRect(BG_A, 0, 0, 128, 32); + VDP_clearTileMapRect(BG_B, 0, 0, 128, 32); +} + void loadGame(){ loadBackground(); loadPlayer(); loadChrome(); - loadStage(); + loadLevel(0); started = TRUE; } static void updateGame(){ updateChrome(); updateSfx(); + if(levelClearing){ + levelClearClock++; + if(levelClearClock == 1){ + clearLevel(); + } + if(levelClearClock >= 120){ + levelClearing = FALSE; + loadBackground(); + loadChrome(); + loadLevel(level + 1); + SPR_setVisibility(player.image, VISIBLE); + } + return; + } if(!paused){ updatePlayer(); if(clock % 2 == 0){ updateEnemies(); - if(!gameOver && enemyCount == 0) gameOver = TRUE; + if(!gameOver && enemyCount == 0){ + if(level >= LEVEL_COUNT - 1){ + gameOver = TRUE; + } else { + levelClearing = TRUE; + levelClearClock = 0; + } + } updateHumans(); } else { updateBackground(); diff --git a/src/player.h b/src/player.h index 12b06f4..e91efe1 100644 --- a/src/player.h +++ b/src/player.h @@ -130,6 +130,11 @@ void loadPlayer(){ } void updatePlayer(){ + if(waitForRelease){ + if(!ctrl.a && !ctrl.b && !ctrl.c && !ctrl.start) + waitForRelease = FALSE; + return; + } if(!gameOver){ if(player.recoveringClock > 0){ if(player.recoveringClock % 10 == 1) diff --git a/src/stage.h b/src/stage.h index f808115..ce32a17 100644 --- a/src/stage.h +++ b/src/stage.h @@ -1,15 +1,102 @@ -void loadStage(){ - // Spawn 3 enemies per zone (4 zones = 12 total) - for(u8 zone = 0; zone < 4; zone++){ - for(u8 i = 0; i < 3; i++){ - spawnEnemy(0, zone); +struct LevelDef { + u8 drones, gunners, hunters, builders; + u8 bossHp; + u8 humans; + u8 gunnerPattern; // 0=radial, 1=aimed fan, 2=mix + bool dronesShoot; +}; + +// dr gn hn bl boss hum pat shoot +const struct LevelDef levels[30] = { + // Phase 1: "Immediate danger" (L1-L6) + { 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1 + { 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2 + { 12, 2, 0, 0, 0, 8, 0, TRUE }, // L3 + { 10, 3, 0, 0, 0, 8, 1, TRUE }, // L4 + { 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) + { 10, 0, 0, 1, 0, 8, 0, TRUE }, // L7 + { 10, 2, 0, 1, 0, 8, 0, TRUE }, // L8 + { 12, 0, 0, 2, 0, 8, 0, TRUE }, // L9 + { 14, 3, 0, 1, 0, 8, 1, TRUE }, // L10 WALL + { 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) + { 8, 0, 4, 0, 0, 8, 0, TRUE }, // L13 + { 8, 3, 2, 0, 0, 8, 1, TRUE }, // L14 + { 16, 0, 0, 0, 0, 8, 0, TRUE }, // L15 FARM + { 10, 2, 4, 0, 0, 8, 2, TRUE }, // L16 + { 12, 3, 3, 0, 0, 8, 1, TRUE }, // L17 + { 0, 2, 2, 0, 15, 8, 2, TRUE }, // L18 BOSS + + // Phase 4: "Suffocation" (L19-L24) + { 12, 4, 0, 0, 0, 8, 2, TRUE }, // L19 + { 14, 4, 0, 2, 0, 8, 2, TRUE }, // L20 WALL + { 10, 0, 6, 0, 0, 8, 0, TRUE }, // L21 + { 12, 4, 2, 0, 0, 8, 1, TRUE }, // L22 + { 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) + { 16, 0, 4, 0, 0, 8, 0, TRUE }, // L25 + { 12, 6, 0, 0, 0, 8, 2, TRUE }, // L26 + { 14, 2, 4, 0, 0, 8, 1, TRUE }, // L27 + { 16, 4, 0, 2, 0, 8, 2, TRUE }, // L28 + { 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 + +static void distributeEnemies(u8 type, u8 count){ + for(u8 i = 0; i < count; i++){ + u8 zone = i % 4; + spawnEnemy(type, zone); + } +} + +void loadLevel(u8 lvl){ + if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1; + level = lvl; + const struct LevelDef* def = &levels[lvl]; + + distributeEnemies(ENEMY_TYPE_DRONE, def->drones); + distributeEnemies(ENEMY_TYPE_GUNNER, def->gunners); + distributeEnemies(ENEMY_TYPE_HUNTER, def->hunters); + distributeEnemies(ENEMY_TYPE_BUILDER, def->builders); + + // set gunner pattern based on level def + for(s16 i = 0; i < ENEMY_COUNT; i++){ + if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){ + if(def->gunnerPattern == 2) + enemies[i].ints[0] = random() % 2; + else + enemies[i].ints[0] = def->gunnerPattern; } } - // Spawn 2 humans per zone (4 zones = 8 total) - for(u8 zone = 0; zone < 4; zone++){ - for(u8 i = 0; i < 2; i++){ + + if(def->bossHp > 0){ + pendingBossHp = def->bossHp; + spawnEnemy(ENEMY_TYPE_BOSS, 1); + } + + // spawn humans + u8 humansToSpawn = def->humans; + for(u8 zone = 0; zone < 4 && humansToSpawn > 0; zone++){ + u8 perZone = humansToSpawn >= 4 ? 2 : 1; + for(u8 h = 0; h < perZone && humansToSpawn > 0; h++){ spawnHuman(zone); + humansToSpawn--; } } + loadMap(); -} \ No newline at end of file +} + +// legacy test stage +void loadStage(){ + loadLevel(0); +} diff --git a/src/start.h b/src/start.h index 6ed936c..422fcf2 100644 --- a/src/start.h +++ b/src/start.h @@ -9,6 +9,7 @@ void loadStart(){ void updateStart(){ if(ctrl.a || ctrl.b || ctrl.c || ctrl.start){ VDP_clearTileMapRect(BG_A, 0, 0, 40, 28); + waitForRelease = TRUE; loadGame(); } } \ No newline at end of file