Skip to content

Cycle 21 — "The Chasm" Content + Survival Rework (Design)

Cycle 21 — “The Chasm” Content + Survival Rework (Design)

Section titled “Cycle 21 — “The Chasm” Content + Survival Rework (Design)”

Date: 2026-06-25 Status: Approved (design); spec under review before planning.

Port the best unbuilt enemies and bosses from Chris’s earlier game The Chasm into Bullet Heaven, and rework survival pacing so fights are fewer-but-deadlier with clean boss arenas and arena-wide enemy movement. Survival-mode only this cycle (story integration is a later data-authoring pass).

In:

  • Part A — Survival spawn & balance rework: clean arena around every boss, fewer/deadlier enemies overall, arena-wide enemy movement.
  • Part B — New enemies: Ghost (telegraphed teleport-strike), Accumulator (grows until killed).
  • Part C — Tank rework: killable homing missiles.
  • Part D — Three new bosses (extend the survival boss rotation 2→5): FunZo (zone-flooding), Graviton (gravity pull), The Eye (predictive lasers) — each enriched with extra mechanics (artistic license granted).

Out (deferred, documented):

  • The “partial” Chasm enemies (Skeleton circle-at-range, Witch glass-cannon dasher, Slime small-dash movement) — easy later adds, not requested.
  • Story-mode authoring of this content (rooms/encounters in story.json).
  • Web demo R2 redeploy decision (Chris’s call at cycle end).
  • “Very slightly homing” dash variant on existing dashers.

Global Constraints (every task inherits these)

Section titled “Global Constraints (every task inherits these)”
  • /sim purity: all new sim logic extends RefCounted, NO Node/Engine/Input/ Time/File/JSON APIs. Loaders/renderers/audio live outside /sim.
  • Determinism is the keystone. Sim ticks on Sim_Const.DT (1/60); all randomness via SeededRng (rng for spawns/sim, upgrade_rng for picks — never cross them). Current pinned baseline (seed 1234, 600 ticks, blade-only): snapshot_string().hash() = 4152236597, state_checksum() = 2325839371.
  • Determinism re-pin policy for THIS cycle: Part A intentionally changes early-window behavior (swarmer spawn rate / damage / movement inside the 0–10s baseline), so Part A does ONE honest re-pin of tests/test_determinism_checksum.gd (capture the new hash+checksum, document old→new in the test comment and CLAUDE.md). The sim stays fully deterministic (same seed → identical trace). Parts B/C/D must NOT further move the (post-A) baseline — new enemies are gated past the 10s window in pick_type, tank-missile fire is window-gated, bosses are story == null + run_time >= 40s. Each chunk re-runs the determinism test; only Part A is allowed to change the pinned values.
  • The “invisible entity” trap: every new entity pool/boss needs a renderer in main.gd; every new fx_events kind needs a matching arm in FxManager.consume. Audit both when adding a pool or fx kind.
  • Telegraph everything (Toby’s fairness rule): with perfect skill the player should take zero damage. Every attack, teleport, dash, pull, and beam gets a visible wind-up/telegraph with a real reaction window. Enrage may shorten the window but never removes it.
  • Deferred death sweep: all enemy damage routes through Sim._damage_enemy (subtract only); a single _sweep_dead() removes. Never remove an enemy mid-query. Hash is rebuilt before every querying phase.
  • Per-chunk ritual (bh-dev-chunk): TDD → --import → boot smoke (grep SCRIPT ERROR; never wrap with timeout, it’s gtimeout on macOS) → full GUT suite → scripts/check-test-count.sh (trust the COUNT) → determinism test → commit. Bump Sim_Const.BUILD per shipped build.
  • Content is DATA: new enemies/bosses get hand-edited data/bible.json entries (tab-indented python round-trip — never re-export from seed.js, it has drifted). Numeric tuning lives in sim.gd constants + bible fields.

Architecture Mapping (how each maps to existing systems)

Section titled “Architecture Mapping (how each maps to existing systems)”
Feature Existing system reused
New enemy types EnemyPool.TYPE_* + BEHAVIOR_* + _build_enemy_types resize + SpawnDirector.pick_type band + main.gd render LUT + ArchetypeRenderer.shape_for
New enemy movement a _step_* dispatched from _move_enemies
Tank missiles new pooled enemy type (low HP) with BEHAVIOR_HOMING, killed like any enemy
New bosses pooled boss entity (TYPE_* / behaviour driven by Sim) + pure-data state holder (cf. BossState/Boss2State) + _update_* driver + a renderer; rotation via _boss_spawn_count % 5
FunZo zones the existing reaction-zones DoT system (or a dedicated funzones array with growth) + ZoneRenderer
Graviton blobs / Eye lasers enemy_proj pool + beam/reaction fx kinds
Telegraphs per-boss render accessors (cf. boss_render_info()) feeding a renderer

Boss scheduling today: _next_boss_time starts at BOSS_FIRST_TIME=40, +BOSS_INTERVAL=80 after each boss dies; alternates Warden/Boss2 by _boss_spawn_count % 2, gated story == null.


Part A — Survival Spawn & Balance Rework

Section titled “Part A — Survival Spawn & Balance Rework”
  • New const BOSS_QUIET_LEAD: float = 30.0.
  • _spawn_enemies early-returns (no normal spawns) when either:
    • a boss is active: _boss_index() != -1 or _boss2_index() != -1 (extend to the new bosses’ index checks), or
    • within the lead window: run_time >= _next_boss_time - BOSS_QUIET_LEAD.
  • Applies to all five bosses. Their own summon/zone attacks supply adds.
  • Optional polish: a HUD “INCOMING” cue when the quiet window starts (render-side).

A2. Fewer, deadlier enemies — but quicker to kill (endless)

Section titled “A2. Fewer, deadlier enemies — but quicker to kill (endless)”

Philosophy: “deadlier” comes from behaviour, ability, and damage — NOT HP. The tanky archetypes are currently bullet sponges (tank 85, brute 160, elite 60); they should hit harder and act more dangerously but die quicker. Across the board: smaller packs, each more threatening, all with shorter time-to-kill. Starting numbers (conservative; refine from gameplay telemetry):

  • SOFT_ENEMY_CAP: 340 → 140.
  • SpawnDirector.rate_at: cut ~40–50% (e.g. base = 0.45 + run_time*0.03; wave multiplier from 0.12 + 1.0*wave0.12 + 0.7*wave; SURGE_RATE 7 → 4).
  • Cut tanky-enemy HP for faster TTK (bible hp fields): tank 85 → ~55, brute 160 → ~110, elite 60 → ~45 (and the Accumulator’s growth cap kept modest so even a maxed one dies in reasonable time). Light enemies roughly unchanged (swarmer stays ~3 — but note any swarmer HP change moves the baseline).
  • Raise lethality everywhere via behaviour/ability/damage, not HP: contact damage ×~1.6 at spawn; ranged/dash/ability enemies tuned up (faster/heavier shots, tighter dash cadence) so even basic walkers feel dangerous. Keep armor. Bosses unaffected.
  • Net intent: short, sharp engagements — small packs you can clear fast but that punish mistakes hard. The Accumulator is the deliberate exception (kill-it-or-else by design), not a passive sponge.
  • Add a per-enemy flank column to EnemyPool (PackedFloat32Array, swap-removed in lockstep) holding a persistent signed angular offset (e.g. ±0.3..0.9 rad, drawn from rng at spawn).
  • BEHAVIOR_WALK target becomes player.pos rotated by flank * falloff(distance): far away → strong offset (enemies fan out / approach from many angles and circle); close in → offset decays so they still converge to finish. A slow global swirl term makes the field orbit rather than clump.
  • Determinism: drawing flank from rng at spawn + the new target math changes the 0–10s window → folds into the Part-A re-pin.

After A1–A3, run the determinism test, capture the new snapshot_string().hash() and state_checksum(), update tests/test_determinism_checksum.gd + its comment (old→new) and the CLAUDE.md baseline note. This is the cycle’s single re-pin.


  • Element: void or aether (tune). Low-ish HP, killable any time.
  • State machine (reuse dash_phase/dash_timer columns; one extra ghost_target via a Sim-side dict keyed by entity_id, or a flank-style column):
    1. DRIFT — moves toward the player very slowly.
    2. TELEGRAPH — picks a teleport endpoint = player.pos + offset where offset is a fixed vector relative to the player at lock time; the silhouette tracks the player-relative offset each tick (running away doesn’t escape it). Shows a plain-ectoplasm silhouette at the endpoint for GHOST_TELEGRAPH_S (fair window).
    3. STRIKE — teleports to the endpoint and immediately dashes through the player line at high speed for a short burst, then returns to DRIFT.
  • Render: the real ghost always shows a distinct floating “eye” marker so it’s never confused with the silhouette; the silhouette is a separate render cue (an fx_events kind, e.g. ghost_warn, with a matching FxManager arm + a tell at the endpoint). Archetype silhouette = a wispy/teardrop shape.
  • Spawn: pick_type band gated to run_time >= ~30s.

B2. Accumulator (TYPE_ACCUMULATOR, BEHAVIOR_DASH reuse + growth)

Section titled “B2. Accumulator (TYPE_ACCUMULATOR, BEHAVIOR_DASH reuse + growth)”
  • Dashes (charge → straight lunge → recharge) like other dashers, but grows over its lifetime: radius and dash speed climb on a timer (movement speed faster, dash frequency constant — per the notes). Cap the growth so a maxed one is hard but not literally impossible, honoring “dodgeable with perfect skill.”
  • Implementation: a per-enemy grow_t accumulator (reuse a free column or a new one) driving radius/_dash_speed scaling each tick; bigger = more contact damage too.
  • Threat design: ignoring it is punished — it becomes a slow-but-huge wall you can’t dodge, so the player must prioritize the kill.
  • Render: archetype shape that visibly scales (the renderer already supports per-instance radius); a subtle pulsing tell as it nears max size.
  • Spawn: band gated run_time >= ~40s, kept rare (1–2 at a time max via tuning).

Part C — Tank Rework: Killable Homing Missiles

Section titled “Part C — Tank Rework: Killable Homing Missiles”
  • New pooled enemy TYPE_TANK_MISSILE (tiny, low HP, fast) with BEHAVIOR_HOMING: turns toward the player at a limited turn rate (cf. BOSS_MISSILE_TURN), expires on a lifetime, deals contact damage, and dies to any player weapon (it’s a real pooled enemy, so weapons + the deferred-death sweep handle it for free).
  • The tank (_update_armed_enemies or a dedicated step keyed by entity_id) periodically fires TANK_MISSILE_COUNT missiles at the player. Telegraph the volley.
  • Determinism: tanks barely appear before 20s, but gate the fire so it cannot trigger inside the post-A baseline window (e.g. only fire when run_time >= 20). Missiles are spawned enemies → they don’t enter the blade-only baseline trace.
  • Render: a small threat-red missile archetype + a launch fx.
  • i-frames: missile contact damage routes through _hurt_player and respects is_invulnerable() like every other damage site.

Part D — Three New Bosses (rotation 2→5)

Section titled “Part D — Three New Bosses (rotation 2→5)”

Wire all five into _boss_spawn_count % 5 (0=Warden, 1=Boss2, 2=FunZo, 3=Graviton, 4=Eye). Each new boss: a pooled boss entity (weapons damage it free), a pure-data state holder, an _update_* driver, a renderer with telegraphs, and a HUD HP bar via a *_render_info() accessor. All spawn story == null + run_time >= _next_boss_time, in a clean arena (Part A).

D1. FunZo (FunZoState) — the zone-flooding clown

Section titled “D1. FunZo (FunZoState) — the zone-flooding clown”
  • Core: summons growing fuchsia DoT zones directly beneath itself; zones grow in radius over time, DoT is small but stacks (tint deepens with overlap density); alternates ~5–10s between slow-drift and fast-dash; grows bigger as HP drops; on death all zones shrink+vanish fast. Strategy: bait it onto already-saturated ground so new zones overlap instead of covering fresh area.
  • Enrichments (artistic license):
    • Jester adds — periodically pops a one-hit “jester” minion out of a zone (a corral/pressure unit; reuses the pooled swarm, spawned by the boss).
    • Confetti burst — each dash landing scatters a telegraphed ring of short-lived blob projectiles (enemy_proj) — punishes standing in the landing spot.
    • Enrage — below FUNZO_ENRAGE_FRAC, zone-spawn rate spikes and dash cadence quickens (window still telegraphed).
  • Reuse: zone DoT + ZoneRenderer; dash telegraph; enemy_proj.

D2. Graviton (GravitonState) — gravity & darkness

Section titled “D2. Graviton (GravitonState) — gravity & darkness”
  • Core: slow approach; fires radial dark-blob projectiles continuously with gaps (enemy_proj); every ~3s applies a gravity pull toward itself — an additive displacement to the player so the player can still steer (vectors add), strength growing as HP drops. The pull is telegraphed (a wind-up ring) so you can pre-position. Blob spread is tuned to leave “ride the gap” safe lanes (the notes’ exact strategy: keep the pull from lining you up with a blob).
  • Enrichments:
    • Singularity Collapse (enrage ultimate) — at GRAVITON_ENRAGE_FRAC it charges a big telegraphed implosion: a strong pull, then it reverses into an outward push + a radial shockwave ring of blobs. Dodge by being at mid-distance when it flips. A genuine “moment,” fully telegraphed.
    • Orbiting satellite blobs — a few blobs orbit Graviton as a moving hazard/shield (reuse the orbiter shard idea) so the radial fire has structure.
  • Reuse: enemy_proj; orbiter math; a pull applied to player.pos (composes with input). Pull respects nothing else’s determinism (boss-only).

D3. The Eye (EyeState) — predictive sight

Section titled “D3. The Eye (EyeState) — predictive sight”
  • Core: predictive-aim lasers that lead the player’s velocity to where they’d be — but each beam has a clear telegraph + reaction window so a skilled player can break their predicted path (resolves “perfect aim vs dodgeable”). The window shrinks and laser speed rises as HP drops. Dashes lazily, mostly hanging near the arena edge / off-camera; a dash that connects deals huge damage.
  • Enrichments:
    • Blink reposition — periodically teleports (telegraphed) to a new arena-edge position to re-angle its lasers and stay off-camera.
    • Multi-beam fan — at low HP fires 2–3 leading beams in a fan (each telegraphed) so you can’t juke a single prediction.
    • Pupil tell — the eye’s pupil visibly tracks its next firing endpoint (diegetic readability: “it’s looking where it’ll shoot”).
    • Dash afterimage line — the dash leaves a brief damaging afterimage along its path (telegraphed), reinforcing dodge-the-line.
  • Reuse: beam fx (keys MUST be pos/dir/length/element); dash math.

  • Renderers: Ghost, Accumulator (scaling), Tank-missile, FunZo, Graviton, Eye each get a renderer + an ArchetypeRenderer silhouette (size the _TYPE_SHAPE LUT to the new max type id; non-convex shapes fan from centre).
  • fx kinds: add arms for any new fx (ghost_warn, FunZo confetti, Graviton collapse, Eye blink/pupil) in FxManager.consume — or reuse existing kinds (reaction/beam/bolt) where they fit.
  • Audio: map new fx kinds to existing AudioManager sounds (SOUND_FOR_FX) + discrete hooks for boss telegraphs/teleports where it adds punch (placeholder chess-defense SFX is fine).
  • HUD: new bosses each show an HP bar via their *_render_info(); max_hp reads the boss’s own spawn HP (not a constant — the build-48 bug fix).
  1. Part A: implement A1–A3, then A4 re-pins the baseline ONCE (old 4152236597/ 2325839371 → new values, documented).
  2. Parts B/C/D: each gated/guarded so the post-A baseline holds byte-identical. Every chunk re-runs the determinism test; if any non-A chunk moves it, that’s a bug to fix (an ungated early-window draw), not a re-pin.
  • Pure /sim unit tests (GUT, headless) for: clean-arena suppression logic, flank target math, Ghost/Accumulator/missile/boss state machines (phase transitions, telegraph windows, growth caps, pull compositing, predictive-lead math). Test the non-erroring seams; consume expected push_errors.
  • Determinism test after every chunk (only A re-pins).
  • Boot smoke (--quit-after, grep SCRIPT ERROR) after every chunk.
  • Count guard (scripts/check-test-count.sh) — trust the COUNT.
  • Feel (telegraph readability, pull compositing, predictive fairness, archetype silhouettes, audio) verified by playtest on the TV after bh-deploy.
  1. Part A — clean-arena window + fewer/deadlier + arena movement + re-pin.
  2. Ghost
  3. Accumulator
  4. Tank killable missiles
  5. FunZo (+ enrichments)
  6. Graviton (+ enrichments)
  7. The Eye (+ enrichments)
  8. 5-way boss rotation wiring + integration + balance pass (bump BUILD).
  9. Deploy (Apple TV via bh-deploy) + memory/CLAUDE.md/site update; web demo redeploy = Chris’s call.