Skip to content

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).

  • /sim purity: 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 are story==null+run_time>=40). Every task re-runs tests/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 new fx_events kind → an arm in FxManager.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, respecting is_invulnerable().
  • Content is DATA: hand-edit data/bible.json (tab-indented python round-trip; never re-export from seed.js). Tuning constants in sim.gd.
  • Per-chunk ritual: godot --headless --path . --import (after any new class_name in a new dir) → boot smoke godot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR" (must be empty; never timeout) → full suite godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexitbash scripts/check-test-count.sh → determinism test → commit.
  • GUT gotchas: an un-asserted push_error fails the test (consume with assert_push_error); assertion typos (assert_leassert_lte) silently drop the file — trust the COUNT; an inner-class member named ready fails compile; := across or with 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 (add BOSS_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_at cut; SOFT_ENEMY_CAP is in sim.gd)
  • Modify: sim/enemy_pool.gd (flank column, 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), update tests/test_determinism_checksum.gd

Interfaces:

  • Produces: Sim.BOSS_QUIET_LEAD; EnemyPool.flank: PackedFloat32Array (added in add, swapped in remove_at); a private Sim._spawn_suppressed() -> bool predicate; 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) OR run_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; assert enemies.count doesn’t grow across ticks while suppressed.

  • Fewer/deadlier-but-quicker (A2): SOFT_ENEMY_CAP 340→140; rate_at cut ~40–50% (base=0.45+run_time*0.03; wave mult 0.12+0.7*wave; SURGE_RATE 7→4). Cut tanky HP in bible.json (tank 55, brute 110, elite 45). Raise contact damage ×~1.6 at spawn (extend the existing per-spawn path — _vary_stats or a new _apply_lethality); keep armor. Bosses unaffected. Test: spawned tanky HP matches the new bible values; contact damage scaled; rate_at lower than the old curve at sampled times.

  • Arena movement (A3): add flank column to EnemyPool (drawn from rng at spawn, e.g. rng.randf_range(0.3,0.9) * (±1)). In the BEHAVIOR_WALK target, rotate the to-player vector by flank * falloff(distance) (strong offset when far → fan/circle; decays near the player → converge). Test: with a non-zero flank, 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(), update tests/test_determinism_checksum.gd + comment (old 4152236597/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_types resize; _enemy_type_*; emit a ghost_warn fx with the silhouette endpoint)
  • Modify: sim/spawn_director.gd (pick_type band, gated run_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_warn arm — 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; hold GHOST_TELEGRAPH_S) → STRIKE (teleport to endpoint, dash through for a short burst) → DRIFT. Reuse dash_phase/dash_timer; store the locked offset per-ghost (a Sim dict keyed by entity_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.

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_homing rotates vel toward the player at TANK_MISSILE_TURN rad/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_COUNT missiles every TANK_FIRE_S, keyed by entity_id (swap-remove safe), telegraphed, gated run_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, respects is_invulnerable(). Determinism unchanged (gate + spawned-enemy). Commit.

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 hp falls; death clears zones.
  • Enrichments (TDD where logic-bearing): jester add popped on a cadence (pooled, one-hit); confetti ring of short-lived enemy_proj on each dash landing (telegraphed); enrage below FUNZO_ENRAGE_FRAC spikes 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 as hp falls; the pull is additive (a player moving away is slowed, not teleported); telegraph precedes the pull.
  • Blobs + satellites (TDD): continuous radial enemy_proj with 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.

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 as hp falls 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_info exposes 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_count 0..4 selects the five bosses in order; only one boss alive at a time; _next_boss_time advances 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 no SCRIPT ERROR and 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 is story==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 + whole tests/), 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.