Cycle 21 — "The Chasm" Content + Survival Rework — Implementation Plan
Cycle 21 — “The Chasm” Content + Survival Rework — Implementation Plan
Section titled “Cycle 21 — “The Chasm” Content + Survival Rework — Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Each task below is one
bh-dev-chunk(TDD → import → boot smoke → full GUT → count guard → determinism → commit). Steps use- [ ].
Goal: Port The Chasm’s best unbuilt enemies/bosses into Bullet Heaven and rework survival pacing (clean boss arenas, fewer/deadlier/quicker-to-kill enemies, arena-wide movement). Survival-mode only.
Architecture: Godot 4.6 typed GDScript; pure deterministic /sim (RefCounted, constant DT, SeededRng); data-oriented EntityPool + MultiMesh/Archetype rendering; deferred death sweep; pooled bosses driven by pure-data state holders. New enemies = TYPE_*+BEHAVIOR_*+bible entry+pick_type band+renderer/silhouette. New bosses = pooled entity + state holder + _update_* + renderer, rotation via _boss_spawn_count % 5.
Tech Stack: Godot 4.6.3, GDScript, GUT 9.6.0 (headless).
Global Constraints
Section titled “Global Constraints”/simpurity: RefCounted, no Node/Engine/Input/Time/File/JSON. Loaders/renderers/audio outside/sim.- Determinism baseline (seed 1234, 600 ticks, blade-only) currently
snapshot_string().hash()=4152236597,state_checksum()=2325839371. ONLY Task 1 (Part A) may re-pin these — it intentionally changes the early window. Tasks 2–8 must hold the post-A baseline byte-identical (gate new enemies past 10s; window-gate tank-missile fire; bosses arestory==null+run_time>=40). Every task re-runstests/test_determinism_checksum.gd. - Two RNG streams:
rng(spawns/sim),upgrade_rng(picks). Never cross. - Invisible-entity trap: every new pool/boss → a renderer in
main.gd; every newfx_eventskind → an arm inFxManager.consume. Audit both. - Telegraph everything (perfect skill = zero damage). Enrage may shorten a window, never remove it.
- All enemy damage via
Sim._damage_enemy; removal only in_sweep_dead. All player damage via_hurt_player, respectingis_invulnerable(). - Content is DATA: hand-edit
data/bible.json(tab-indented python round-trip; never re-export fromseed.js). Tuning constants insim.gd. - Per-chunk ritual:
godot --headless --path . --import(after any newclass_namein a new dir) → boot smokegodot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR"(must be empty; nevertimeout) → full suitegodot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit→bash scripts/check-test-count.sh→ determinism test → commit. - GUT gotchas: an un-asserted
push_errorfails the test (consume withassert_push_error); assertion typos (assert_le≠assert_lte) silently drop the file — trust the COUNT; an inner-class member namedreadyfails compile;:=acrossorwith a narrowed event fails compile (silent freeze).
Task 1: Survival spawn & balance rework (+ the one re-pin)
Section titled “Task 1: Survival spawn & balance rework (+ the one re-pin)”Files:
- Modify:
sim/sim.gd(addBOSS_QUIET_LEAD; suppression in_spawn_enemies; lethality at spawn; flank-target math in the WALK branch of_move_enemies) - Modify:
sim/spawn_director.gd(rate_atcut;SOFT_ENEMY_CAPis in sim.gd) - Modify:
sim/enemy_pool.gd(flankcolumn, swap-removed in lockstep) - Modify:
data/bible.json(tanky HP: tank 85→55, brute 160→110, elite 60→45) - Test:
tests/test_spawn_rework.gd(new), updatetests/test_determinism_checksum.gd
Interfaces:
-
Produces:
Sim.BOSS_QUIET_LEAD;EnemyPool.flank: PackedFloat32Array(added inadd, swapped inremove_at); a privateSim._spawn_suppressed() -> boolpredicate; new pinned determinism values. -
Clean arena (A1): add
const BOSS_QUIET_LEAD: float = 30.0. In_spawn_enemies, early-return when a boss is active (_boss_index() != -1 or _boss2_index() != -1) ORrun_time >= _next_boss_time - BOSS_QUIET_LEAD. Factor the predicate so it’s unit-testable. Test: suppressed within the lead window, when a boss is active, and NOT during normal play; assertenemies.countdoesn’t grow across ticks while suppressed. -
Fewer/deadlier-but-quicker (A2):
SOFT_ENEMY_CAP340→140;rate_atcut ~40–50% (base=0.45+run_time*0.03; wave mult0.12+0.7*wave;SURGE_RATE7→4). Cut tanky HP inbible.json(tank 55, brute 110, elite 45). Raise contact damage ×~1.6 at spawn (extend the existing per-spawn path —_vary_statsor a new_apply_lethality); keep armor. Bosses unaffected. Test: spawned tanky HP matches the new bible values; contact damage scaled;rate_atlower than the old curve at sampled times. -
Arena movement (A3): add
flankcolumn toEnemyPool(drawn fromrngat spawn, e.g.rng.randf_range(0.3,0.9) * (±1)). In theBEHAVIOR_WALKtarget, rotate the to-player vector byflank * falloff(distance)(strong offset when far → fan/circle; decays near the player → converge). Test: with a non-zeroflank, a far walker’s heading is rotated off the direct line; a near walker’s heading converges. Pure-math seam (no Node). -
Re-pin (A4): run the determinism test, capture new
snapshot_string().hash()+state_checksum(), updatetests/test_determinism_checksum.gd+ comment (old4152236597/2325839371→ new), and note it in CLAUDE.md at integration (Task 8). Verify the suite + count guard green. -
Commit.
Task 2: Ghost enemy (telegraphed teleport-strike)
Section titled “Task 2: Ghost enemy (telegraphed teleport-strike)”Files:
- Modify:
sim/enemy_pool.gd(TYPE_GHOST,BEHAVIOR_GHOST, name) - Modify:
sim/sim.gd(_step_ghost;_build_enemy_typesresize;_enemy_type_*; emit aghost_warnfx with the silhouette endpoint) - Modify:
sim/spawn_director.gd(pick_typeband, gatedrun_time>=~30) - Modify:
data/bible.json(ghost entry: element, hp, speed, color, contact) - Modify:
main.gd(render LUT size to new max type id),render/archetype_renderer.gd(wispy shape),fx/fx_manager.gd(ghost_warnarm — endpoint silhouette tell) - Test:
tests/test_ghost.gd
Interfaces: Consumes EnemyPool columns + _damage_enemy. Produces _step_ghost state machine.
- State machine (TDD): DRIFT (slow toward player) → TELEGRAPH (lock endpoint =
player.pos + offset; recompute the silhouette each tick from the current player-relative offset so running doesn’t escape; holdGHOST_TELEGRAPH_S) → STRIKE (teleport to endpoint, dash through for a short burst) → DRIFT. Reusedash_phase/dash_timer; store the locked offset per-ghost (a Sim dict keyed byentity_id, cleared on death, OR a reused column). Tests: phase transitions on the right timers; the telegraph endpoint tracks the player; STRIKE moves the ghost to the endpoint; back to DRIFT after the burst. - Render/fx: real ghost shows a distinct “eye” marker (archetype/secondary cue); silhouette is a plain endpoint tell via
ghost_warn(FxManager arm). Boot smoke must show the cue (no invisible entity). - Determinism: spawn-gate ≥30s so the blade-only baseline is unaffected; re-run the (post-A) determinism test — must be unchanged. Commit.
Task 3: Accumulator enemy (grows until killed)
Section titled “Task 3: Accumulator enemy (grows until killed)”Files: sim/enemy_pool.gd (TYPE_ACCUMULATOR; reuse BEHAVIOR_DASH; a grow_t column or reuse a free one), sim/sim.gd (growth step scaling radius/_dash_speed/contact over lifetime, capped), sim/spawn_director.gd (band ≥~40s, rare), data/bible.json, main.gd/render/archetype_renderer.gd (scaling shape + near-max pulse tell), tests/test_accumulator.gd.
- Growth (TDD): per-tick
grow_t += dt;radius, dash speed, contact scale up to a cap (ACCUMULATOR_MAX_SCALE). Movement speed faster over time, dash frequency constant. Tests: radius/speed increase over ticks; clamp at the cap; a maxed one is large but its dash is still finite-speed (dodgeable bound). - Render: per-instance radius already supported; add a pulsing tell as it nears max. Spawn-gate ≥40s, keep rare. Determinism unchanged. Commit.
Task 4: Killable homing tank missiles
Section titled “Task 4: Killable homing tank missiles”Files: sim/enemy_proj_pool.gd or a new EnemyPool type — decision: make a pooled enemy TYPE_TANK_MISSILE (tiny, low HP) with BEHAVIOR_HOMING so weapons + _sweep_dead kill it free. sim/enemy_pool.gd (TYPE_TANK_MISSILE, BEHAVIOR_HOMING), sim/sim.gd (_step_homing limited-turn toward player + lifetime; tank fire step keyed by entity_id, window-gated run_time>=20; _build_enemy_types resize), data/bible.json (tank_missile entry; tank gains fire fields), main.gd/render/archetype_renderer.gd (small red missile + launch fx), tests/test_tank_missiles.gd.
- Homing (TDD):
_step_homingrotatesveltoward the player atTANK_MISSILE_TURNrad/s, moves, expires on lifetime. Test: missile heading turns toward a moving player but bounded by the turn rate (can be outrun/dodged); expires. - Tank fire (TDD): tank emits
TANK_MISSILE_COUNTmissiles everyTANK_FIRE_S, keyed byentity_id(swap-remove safe), telegraphed, gatedrun_time>=20. Test: a tank past the gate fires the right count; a tank before the gate fires none. - i-frames: missile contact via
_hurt_player, respectsis_invulnerable(). Determinism unchanged (gate + spawned-enemy). Commit.
Task 5: FunZo boss (zone-flooding clown)
Section titled “Task 5: FunZo boss (zone-flooding clown)”Files: sim/funzo_state.gd (new, pure-data: phases, grow-with-HP, enrage, jester/confetti timers), sim/enemy_pool.gd (TYPE_FUNZO, drive via Sim like other bosses), sim/sim.gd (_spawn_funzo, _update_funzo, _funzo_index, funzo_render_info; zone summon reusing the zones DoT; confetti via enemy_proj; jester adds via the pooled swarm), render/funzo_renderer.gd (body grows with HP + telegraphs), data/bible.json (funzo boss entry), main.gd (renderer + HP bar), fx/fx_manager.gd (confetti/landing arm if new), tests/test_funzo.gd.
- State machine (TDD): alternate slow-drift / fast-dash (~5–10s); summon growing DoT zones beneath itself on a cadence; body radius scales as HP drops; on death zones shrink+vanish fast. Tests: phase alternation; a zone is summoned at the boss position; body radius grows as
hpfalls; death clears zones. - Enrichments (TDD where logic-bearing): jester add popped on a cadence (pooled, one-hit); confetti ring of short-lived
enemy_projon each dash landing (telegraphed); enrage belowFUNZO_ENRAGE_FRACspikes zone rate + dash cadence. Tests: enrage latches once; zone cadence faster post-enrage; jester spawned. - Render/HUD: body + zone tint stacking (reuse ZoneRenderer); HP bar via
funzo_render_info(max_hp= its own spawn HP). Pooled so weapons damage it free. NOT spawned yet (rotation wired in Task 8) — test via direct_spawn_funzo. Determinism unchanged. Commit.
Task 6: Graviton boss (gravity & darkness)
Section titled “Task 6: Graviton boss (gravity & darkness)”Files: sim/graviton_state.gd (new: approach, pull cadence/strength, Singularity Collapse sub-phases, satellites), sim/enemy_pool.gd (TYPE_GRAVITON), sim/sim.gd (_spawn_graviton, _update_graviton, _graviton_index, graviton_render_info; radial blob fire via enemy_proj with tuned gaps; gravity pull as an additive displacement to player.pos that composes with input; collapse ultimate), render/graviton_renderer.gd (body + pull wind-up ring + collapse telegraph), data/bible.json, main.gd, tests/test_graviton.gd.
- Pull (TDD): every
GRAVITON_PULL_S, telegraph then apply an additive pull vector toward the boss to the player; strength scales as HP drops. Player input still applies (vectors add). Tests: pull magnitude grows ashpfalls; the pull is additive (a player moving away is slowed, not teleported); telegraph precedes the pull. - Blobs + satellites (TDD): continuous radial
enemy_projwith gaps (safe lanes exist — assert a gap in the fired angular set); a few satellite blobs orbit the boss (orbiter math). - Singularity Collapse (TDD): below
GRAVITON_ENRAGE_FRAC, charge (telegraph) → strong pull → reverse into outward push + radial shockwave ring of blobs. Tests: the collapse sequences charge→pull→push; the push reverses the pull sign; ring of blobs emitted. - Render/HUD; pooled; not spawned yet. Determinism unchanged. Commit.
Task 7: The Eye boss (predictive sight)
Section titled “Task 7: The Eye boss (predictive sight)”Files: sim/eye_state.gd (new: lazy dash, blink, laser lead + window, multi-beam, pupil target), sim/enemy_pool.gd (TYPE_EYE), sim/sim.gd (_spawn_eye, _update_eye, _eye_index, eye_render_info; predictive-lead laser via beam fx with keys pos/dir/length/element; blink teleport; dash + afterimage damage line), render/eye_renderer.gd (body + pupil tracking next endpoint + beam telegraphs + dash afterimage), data/bible.json, main.gd, tests/test_eye.gd.
- Predictive laser (TDD): compute the lead target from the player’s velocity (where they’d be at fire time); telegraph for
EYE_WINDOW_S(shrinks as HP drops) before firing; laser travel/sweep speed rises as HP drops. Tests: the lead target is ahead of the player along their velocity; the window shrinks ashpfalls but stays> 0(dodgeable bound). - Blink + lazy dash (TDD): periodically blink (telegraphed) to an arena-edge position to re-angle; dash lazily, mostly toward the edge; a connecting dash deals huge damage; the dash leaves a brief damaging afterimage line (telegraphed). Tests: blink moves the eye to an edge; dash damage routes through
_hurt_player+i-frames; afterimage line is time-bounded. - Multi-beam + pupil (TDD): at low HP fire 2–3 leading beams in a fan (each telegraphed);
eye_render_infoexposes the pupil’s next endpoint(s) for the renderer. Tests: beam count increases below the low-HP threshold. - Render/HUD; pooled; not spawned yet. Determinism unchanged. Commit.
Task 8: 5-way rotation wiring + integration + balance pass
Section titled “Task 8: 5-way rotation wiring + integration + balance pass”Files: sim/sim.gd (_boss_spawn_count % 5 → Warden/Boss2/FunZo/Graviton/Eye; the survival spawn block currently in _update_boss/_update_boss2 — route each modulo to the right _spawn_*; ensure _update_funzo/graviton/eye run each tick; clean-arena suppression already covers them via their _*_index()), sim/constants.gd (BUILD bump), CLAUDE.md (new cycle section + re-pinned baseline), tests/test_boss_rotation.gd.
- Rotation (TDD): stepping
_boss_spawn_count0..4 selects the five bosses in order; only one boss alive at a time;_next_boss_timeadvances on each death (existing). Test: drive the counter and assert the correct_spawn_*fires for each residue; assert spawns are suppressed while any boss is alive. - Integration smoke: a longer headless run (e.g.
--quit-after 4000) shows noSCRIPT ERRORand exercises a boss; manual reasoning that clean-arena + new bosses + new enemies coexist. - Balance pass: sanity-check the Part-A numbers against the new content (tanky TTK, pack size, boss HP); leave hooks for telemetry refinement. Bump
Sim_Const.BUILD. Determinism unchanged (rotation isstory==null+time-gated). Commit.
Task 9: Deploy + docs (controller-run, not a subagent)
Section titled “Task 9: Deploy + docs (controller-run, not a subagent)”-
bh-deploy: sync main→tvOS (changed gameplay files + wholetests/), bump BUILD in both, verify tvOS repo (import/boot/suite/count), export.pck, xcodebuild,devicectl install. - Update CLAUDE.md (cycle 21 section, re-pinned baseline), memory (
bullet_heaven_game.md), and the site legend/changelog (new enemies + bosses) — site/web-demo redeploy is Chris’s call. - Final whole-branch review (opus) over
merge-base..HEAD; dispatch ONE fix subagent for any Critical/Important findings.