From 4036b5f07e69daf325bae53b669a2d7039e9d75b Mon Sep 17 00:00:00 2001 From: Trevor Boddy Date: Tue, 17 Feb 2026 17:40:34 -0500 Subject: [PATCH] level design etc --- flower.fur | Bin 0 -> 361 bytes res/human.png | Bin 4106 -> 4582 bytes res/koakuma.png | Bin 0 -> 5266 bytes res/resources.res | 1 + src/chrome.h | 13 +++ src/enemies.h | 49 +++++++-- src/enemytypes.h | 246 +++++++++++++++++++++++++++++++++++++++++++++- src/global.h | 19 +++- src/humans.h | 2 +- src/main.c | 40 +++++++- src/player.h | 5 + src/stage.h | 105 ++++++++++++++++++-- src/start.h | 1 + 13 files changed, 460 insertions(+), 21 deletions(-) create mode 100644 flower.fur create mode 100644 res/koakuma.png diff --git a/flower.fur b/flower.fur new file mode 100644 index 0000000000000000000000000000000000000000..84c0ab4ed0fd32e64ba726a9c0138b9a109b681f GIT binary patch literal 361 zcmb=Jb9T~xze52c?dLPS7ygOAQ^}b0h?~R9fM-!)d_zWZ2UiQ5XNh59Y1s$P8m9RN zj-Iu!zH{;9k=W+=gHB#b7FD<2Kl^v9?)$xa+hXLuSg&>7UvkY>YR<2x=7O3h*GQfH zb#Aij@zT&G+XDqfms{+d-j*FL7MAL%yY0XG-n_2V%p1YI>sGG&@XWk`{rd-o{trxS z_Z}K=v$oQ$j`CLDA2mJW*@u!vt`Uh-qWe7G{yx6;(zN>@o69!rl5LCHU;FMnukrQx z?l3dCH6GC^4;I~c;J_5);lyR*;mo#aLbHvC${*Ev?p$IR;tjEf&3~}ne;{4cod1D$ z-$4Y&*}O`!eBF~~`5)Xi+jd7~zp=FU|99>FS~-*5mmm7MFkjrqc1v9LOjFqwb*m?{ y_>RZ;T-xz>bBX0tw})HaJt^)uGfC9DOviHLlnzz%X_Gci=~C7Ax$iq^@@fEC|FDk$ literal 0 HcmV?d00001 diff --git a/res/human.png b/res/human.png index 10d026dd80fe0052b66a6d7b45a495d3b916b51b..619fdd349cfb7bd64d3b4107728a65512b27cd5c 100644 GIT binary patch delta 480 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 dx$$q}bu>dKV;j#Q= 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