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)”/simpurity: all new sim logicextends 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 viaSeededRng(rngfor spawns/sim,upgrade_rngfor 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 inpick_type, tank-missile fire is window-gated, bosses arestory == 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 newfx_eventskind needs a matching arm inFxManager.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 (grepSCRIPT ERROR; never wrap withtimeout, it’sgtimeouton macOS) → full GUT suite →scripts/check-test-count.sh(trust the COUNT) → determinism test → commit. BumpSim_Const.BUILDper shipped build. - Content is DATA: new enemies/bosses get hand-edited
data/bible.jsonentries (tab-indented python round-trip — never re-export fromseed.js, it has drifted). Numeric tuning lives insim.gdconstants + 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”A1. Clean arena around every boss
Section titled “A1. Clean arena around every boss”- New const
BOSS_QUIET_LEAD: float = 30.0. _spawn_enemiesearly-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.
- a boss is active:
- 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 from0.12 + 1.0*wave→0.12 + 0.7*wave;SURGE_RATE7 → 4).- Cut tanky-enemy HP for faster TTK (bible
hpfields): 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.
A3. Arena-wide movement (flanking layer)
Section titled “A3. Arena-wide movement (flanking layer)”- Add a per-enemy
flankcolumn toEnemyPool(PackedFloat32Array, swap-removed in lockstep) holding a persistent signed angular offset (e.g.±0.3..0.9 rad, drawn fromrngat spawn). BEHAVIOR_WALKtarget becomesplayer.posrotated byflank * 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
flankfromrngat spawn + the new target math changes the 0–10s window → folds into the Part-A re-pin.
A4. Re-pin
Section titled “A4. 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.
Part B — New Enemies
Section titled “Part B — New Enemies”B1. Ghost (TYPE_GHOST, BEHAVIOR_GHOST)
Section titled “B1. Ghost (TYPE_GHOST, BEHAVIOR_GHOST)”- Element: void or aether (tune). Low-ish HP, killable any time.
- State machine (reuse
dash_phase/dash_timercolumns; one extraghost_targetvia a Sim-side dict keyed byentity_id, or aflank-style column):- DRIFT — moves toward the player very slowly.
- TELEGRAPH — picks a teleport endpoint =
player.pos + offsetwhereoffsetis 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 forGHOST_TELEGRAPH_S(fair window). - 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_eventskind, e.g.ghost_warn, with a matching FxManager arm + a tell at the endpoint). Archetype silhouette = a wispy/teardrop shape. - Spawn:
pick_typeband gated torun_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_taccumulator (reuse a free column or a new one) drivingradius/_dash_speedscaling 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) withBEHAVIOR_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_enemiesor a dedicated step keyed byentity_id) periodically firesTANK_MISSILE_COUNTmissiles 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_playerand respectsis_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_FRACit 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.
- Singularity Collapse (enrage ultimate) — at
- Reuse:
enemy_proj; orbiter math; a pull applied toplayer.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:
beamfx (keys MUST bepos/dir/length/element); dash math.
Cross-Cutting
Section titled “Cross-Cutting”- Renderers: Ghost, Accumulator (scaling), Tank-missile, FunZo, Graviton, Eye each
get a renderer + an
ArchetypeRenderersilhouette (size the_TYPE_SHAPELUT 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) inFxManager.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_hpreads the boss’s own spawn HP (not a constant — the build-48 bug fix).
Determinism Plan (summary)
Section titled “Determinism Plan (summary)”- Part A: implement A1–A3, then A4 re-pins the baseline ONCE (old
4152236597/2325839371→ new values, documented). - 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.
Testing Strategy
Section titled “Testing Strategy”- Pure
/simunit tests (GUT, headless) for: clean-arena suppression logic,flanktarget math, Ghost/Accumulator/missile/boss state machines (phase transitions, telegraph windows, growth caps, pull compositing, predictive-lead math). Test the non-erroring seams; consume expectedpush_errors. - Determinism test after every chunk (only A re-pins).
- Boot smoke (
--quit-after, grepSCRIPT 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.
Sequencing (chunks)
Section titled “Sequencing (chunks)”- Part A — clean-arena window + fewer/deadlier + arena movement + re-pin.
- Ghost
- Accumulator
- Tank killable missiles
- FunZo (+ enrichments)
- Graviton (+ enrichments)
- The Eye (+ enrichments)
- 5-way boss rotation wiring + integration + balance pass (bump BUILD).
- Deploy (Apple TV via
bh-deploy) + memory/CLAUDE.md/site update; web demo redeploy = Chris’s call.