Biomass Wave Spawning — Implementation Plan
Biomass Wave Spawning — Implementation Plan
Section titled “Biomass Wave Spawning — Implementation Plan”Sub-skill: implement task-by-task with the
bh-dev-chunkritual (TDD → import → boot → full suite → count guard → determinism). One task = one commit.
Goal: Replace the cycle-22b survival ramp/boss-rotation with a wave→clear→boss→repeat loop driven
by a biomass pressure score. Spec: docs/superpowers/specs/2026-06-28-biomass-wave-spawning-design.md.
Architecture: A biomass column on EnemyPool + Sim.total_biomass(); a time→type probability
SpawnTable; a Sim.spawn_phase state machine (WAVES→WIND_DOWN→BOSS_PREP→BOSS→REST); waves fire when
biomass < 100 and fill to ≥400 (elites first, then probabilistic). Warden/Boss2 → elites (spawn in
waves); FunZo/Graviton/Eye = the BOSS-phase pool. Survival + crystal only (story == null).
Branch: build in an isolated worktree off main so the Wed iOS launch (current build) is unaffected;
merge after launch.
Global constraints
Section titled “Global constraints”/simstays pure (RefCounted, no Node/Engine/Input/Time/OS/File/JSON). New render-read helpers are pure + excluded fromsnapshot_string()/state_checksum().- Story/tutorial untouched (all new spawn logic guarded by
story == null). - The spawner rewrite re-pins both determinism baselines once (Task 4) — honest, documented.
- Tunables (biomass scores, thresholds, timers, elite times, probability table) live as
constants/
bible.jsondata.
Task 1: Biomass column + scores + total_biomass()
Section titled “Task 1: Biomass column + scores + total_biomass()”Files: sim/enemy_pool.gd (new biomass PackedInt32Array column, swapped in add/remove_at),
sim/sim.gd (set biomass on spawn; total_biomass()), data/bible.json (biomass per enemy),
tests/test_biomass.gd (new).
Interfaces produced: EnemyPool.biomass: PackedInt32Array; Sim.total_biomass() -> int;
Sim.spawn_enemy(... biomass) sets the column (0 for minions/bosses).
- Test: spawn 3 swarmers (biomass 2 each) →
total_biomass()==6; remove one → 4; a boss/minion adds 0. - Add the column (resize in
_init, set inadd, swap inremove_at); addbiomassto bible enemies (swarmer 2 … accumulator 16; Warden 110, Boss2 100; bosses/minions 0). -
_spawn_one/spawn paths pass the type’s biomass;total_biomass()sums the column. - Gates green (determinism UNCHANGED — column is additive, not in the checksum). Commit.
Task 2: SpawnTable (time→type probability vectors)
Section titled “Task 2: SpawnTable (time→type probability vectors)”Files: sim/spawn_table.gd (new, RefCounted), tests/test_spawn_table.gd (new).
Interfaces produced: SpawnTable.weights_at(t) -> Dictionary{type_id:weight} (lerped, sums≈1);
SpawnTable.pick(t, rng) -> int (samples a type deterministically); endless tail loops.
- Test: each authored 30s vector sums to ~1;
weights_at(45)lerps the 30s/60s entries;pickis deterministic for a seed; out-of-range t loops the endless tail. - Author the table (early swarmer/spider → mid +shooter/scatterer/ghost/orbiter → late +lancer/bomber/ accumulator/brute/pyromancer; zapper/rusher stay out). Commit.
Task 3: Spawn phase machine + timers
Section titled “Task 3: Spawn phase machine + timers”Files: sim/sim.gd (enum + spawn_phase, timers, _update_spawn_phase(dt), phase-change events),
tests/test_spawn_phase.gd (new).
Interfaces produced: Sim.spawn_phase: int (WAVES/WIND_DOWN/BOSS_PREP/BOSS/REST), Sim.phase_timer,
render-read Sim.spawn_banner() -> {text, seconds}.
- Test: starts WAVES; at
WAVE_PHASE_S→ WIND_DOWN; biomass 0 in WIND_DOWN → BOSS_PREP(10s) →BOSS; boss-dead → REST(10s) → WAVES. (Drive via direct state +_update_spawn_phase, story==null.) - Implement the machine (guarded
story==null); emit banner text on transitions. Determinism: the machine only drives spawning in Task 4, so this is inert until then. Commit.
Task 4: Wave generation (THE spawner replacement) + determinism re-pin
Section titled “Task 4: Wave generation (THE spawner replacement) + determinism re-pin”Files: sim/sim.gd (_spawn_enemies → wave logic; remove the cycle-22b ramp/_spawn_suppressed/
swarm-burst/boss-add trickle for survival), tests/test_spawn_rework.gd (rewrite), determinism tests +
CLAUDE.md (re-pin).
- Test: in WAVES, when
total_biomass()<BIOMASS_TRIGGER(100), one wave call fills to>=BIOMASS_TARGET(400); no wave fires while biomass≥100; spawns are off-screen ring. - Implement: trigger + probabilistic fill (sample
SpawnTable.pick,_spawn_one, accumulate biomass) until ≥400; elites-first hook (Task 5 fills it). Replace the old survival spawn body. - Re-run determinism → re-pin
test_determinism_checksum+test_determinism_crystalsto the new hash/checksum; note in CLAUDE.md. Verify run-twice-identical. Commit.
Task 5: Elites (Warden + Boss2 demoted)
Section titled “Task 5: Elites (Warden + Boss2 demoted)”Files: sim/sim.gd (elite spawn-time table; spawn in-wave before fill; remove Warden/Boss2 from the
boss rotation; they carry biomass + don’t clear the arena), tests/test_elites.gd (new).
- Test: an elite due at its hard-coded time spawns in the next wave first; carries its high biomass; the arena isn’t cleared for it.
- Implement:
ELITE_SPAWN_TIMES = {warden: 90, boss2: 210}(tunable); a due-and-unspawned elite is added at wave start. Commit.
Task 6: Boss gate (FunZo / Graviton / Eye)
Section titled “Task 6: Boss gate (FunZo / Graviton / Eye)”Files: sim/sim.gd (BOSS phase spawns one boss from the pool into the cleared arena; on death advance
to REST; rotate the pool), tests/test_boss_gate.gd (new / fold into boss-rotation test).
- Test: entering BOSS spawns exactly one pooled boss (biomass 0); boss death → REST → WAVES; pool rotates.
- Implement; remove the old
_maybe_spawn_survival_bossinterval logic (the phase machine owns it now). Commit.
Task 7: Minions (biomass 0)
Section titled “Task 7: Minions (biomass 0)”Files: sim/sim.gd (boss-summoned adds get biomass 0; FunZo jesters / boss adds routed as minions),
tests/test_minions.gd (new / fold in).
- Test: a boss-summoned enemy has biomass 0 (doesn’t inflate the wave trigger).
- Implement (summon paths set the biomass column to 0). Commit.
Task 8: HUD banners
Section titled “Task 8: HUD banners”Files: ui/hud.gd (read Sim.spawn_banner() → centered “WAVE INCOMING” / “BOSS APPROACHING N” /
“AREA CLEAR”), main.gd (feed it), reuse RegionTitle-style sweep if handy.
- Render-only (no determinism impact). Verify by boot + playtest. Commit.
Task 9: Whole-branch review + merge prep — DONE (review run 2026-06-28)
Section titled “Task 9: Whole-branch review + merge prep — DONE (review run 2026-06-28)”All 5 load-bearing invariants verified clean by the code review (determinism + re-pin, /sim purity,
story == null guard, biomass column lockstep swap-remove, bosses/minions biomass 0). Two Critical
findings, both fixed: added tests/test_boss_gate.gd (the gate had no coverage); replaced the
misleading test_boss_adds_spawn_small_fast_types_only with test_no_standard_waves_during_boss_phase.
Deferred pre-merge cleanup (post-launch — large, ~14 test files): the old spawn mechanism is now DEAD but still asserted by many tests (false confidence). To remove before merge:
- Delete dead code:
SpawnDirector.rate_at/pick_type/pick_boss_add_type/spawn_count_for,_maybe_spawn_survival_boss,_spawn_boss_adds,_spawn_swarm_burst, and the consts/varsSWARM_BURST_*/BOSS_ADD_RATE/BOSS_FIRST_TIME/BOSS_INTERVAL/_next_boss_time/_swarm_burst_fired/_boss_add_accum(+ the 5_next_boss_timeupdates in_sweep_dead). - Delete
test_boss_rotation.gd(tests the dead dispatcher) +test_spawn_director.gdrate tests. - Rewrite the enemy spawn-band tests (test_ghost/brute/accumulator/new_enemies/dash_spider/eye/funzo/
graviton) to assert against
SpawnTable.weights_at(t)instead ofSpawnDirector.pick_type. - Update CLAUDE.md determinism baselines → survival 318766133/2760178738, crystals 545935757/2116139268. Harmless to leave for now (dead code doesn’t run); do it deliberately, not crammed in.
Sequencing notes
Section titled “Sequencing notes”Sequencing notes
Section titled “Sequencing notes”- Tasks 1-3 are additive → suite + determinism stay green. Task 4 is where behavior + the baseline change (re-pin there). 5-7 build on 4. 8 is cosmetic. Each task ends green + committed.