Bullet Heaven — Combat Depth, New Content & Balance
Bullet Heaven — Combat Depth, New Content & Balance
Section titled “Bullet Heaven — Combat Depth, New Content & Balance”Date: 2026-06-25 Status: Design (approved in brainstorming; awaiting spec review) Author: Chris Allen + Claude
1. Overview
Section titled “1. Overview”A multi-feature pass adding combat depth, content, and a balance sweep to the Godot game. Everything is a shared sim/render system used by both Story and Survival unless stated otherwise. Driven by Chris’s request:
new boss with 5 random attacks (incl. a charging beam + arcing rocket-shrapnel); separate the tutorial from the main game and drop the dialogue pauses there; per-enemy attribute variation; tanks never arrive alone; a balance pass on player dmg / enemy dmg / upgrade cost; new enemies that each mirror a player weapon archetype with varied speed; a new relentless enemy dash plus a player thruster/dodge to counter it; visually distinct enemy archetypes; and damage numbers that actually show.
- More legible, more varied combat that reads at a glance.
- A reason to invest in mobility (player thruster vs a no-pause enemy dash).
- A headline boss with five distinct, randomly-selected attacks.
- Sane, costed balance numbers for damage, armor, XP, gold and meta cost.
Non-goals (out of scope)
Section titled “Non-goals (out of scope)”- Multiplayer / Phase 3 (the determinism discipline below keeps the door open).
- Reworking the level-up picker into a non-blocking UI (the picker pause stays — see §2).
- Re-exporting
bible.jsonfromseed.js(it has drifted ahead; hand-editdata/bible.jsonper the project rule). - A full telemetry-driven tune (telemetry is effectively empty: 2 story runs, and
its
avg_dpsmetric is broken ~2,000,000). Numbers here are reasoned + will be finalized by playtest; the broken DPS metric is noted as optional cleanup.
2. Modes & tutorial separation
Section titled “2. Modes & tutorial separation”The start menu gains a third card. Mode is a render/flow concern in main.gd +
ui/start_menu.gd; the sim only knows story == null (survival) vs non-null.
| Card | enable_story flags |
Dialogue freeze | Layout/spawns | Level-up pause |
|---|---|---|---|---|
| TUTORIAL | suppress_tutorial=false, randomize=false |
yes (read the lines) | fixed | yes |
| STORY (main game) | suppress_tutorial=true, randomize=true |
no | randomized | yes |
| SURVIVAL | (story stays null) | n/a | procedural | yes |
- TUTORIAL is today’s taught first run. Completing it (reach region 2 or
finish) sets
meta.tutorial_done = trueexactly as now. - STORY is the main campaign: always suppress tutorials + randomize, and
dialogue never freezes the sim — lines float by non-blocking via the
existing
DialogueBox(time-revealed, auto-advancing; no input gate). - Level-up picker pause is KEPT in all modes (genre-core; Chris confirmed). Only the story-dialogue freeze is removed from the main game.
Implementation:
StartMenu: add a TUTORIAL card;mode_chosencarries an enum/string (tutorial/story/survival) instead of the currentbool story.main.gd: a_modefield. The dialogue-freeze branch (if sim.story != null and dialogue_box.is_showing()) is gated to_mode == TUTORIALonly. Story start uses the table above instead ofdone = meta.tutorial_done.- Determinism-neutral (render/flow only).
3. Player thruster + the new enemy dash (a designed pair)
Section titled “3. Player thruster + the new enemy dash (a designed pair)”3a. Player thruster (dash/dodge)
Section titled “3a. Player thruster (dash/dodge)”- New
InputState.dash: bool(default false → baseline-safe). - Input wiring (
InputRouter/ensure_actions): Space or RB on web/pad; a Siri-remote button on tvOS (pick one not used by decoy on LB/Q). - Mechanic on
Sim+PlayerState: on a rising edge, if off cooldown, the player gets a short committed burst in the current move direction (fallback: last facing) over ~0.16s, plus an i-frame window (~0.18s) during which_hurt_player/ contact / projectile hits are ignored. - Tunables (constants, playtest-final):
DASH_COOLDOWN=1.5,DASH_SPEED≈900,DASH_TIME=0.16,DASH_IFRAMES=0.18. - Upgrades:
- In-run (level-up picker): a stat-mod (
thrusters) that improves cooldown- i-frames, routed through
StatEffects.
- i-frames, routed through
- Meta: a new
Thrustersmeta upgrade inbible.jsonmeta_upgrades.
- In-run (level-up picker): a stat-mod (
PlayerStategainsdash_cooldown_mult,dash_iframe_bonus, plus transientdash_timer,dash_cd,dash_dir,iframe_timer.- HUD: a small thruster-ready indicator (render-only).
- Determinism: no dash input + no i-frame hits in the blade-only baseline →
byte-identical. The hit-skip only branches when
iframe_timer > 0, never true in the baseline.
3b. New enemy dash — “Rusher” (EnemyPool.BEHAVIOR_RUSH)
Section titled “3b. New enemy dash — “Rusher” (EnemyPool.BEHAVIOR_RUSH)”- Fast, no charge telegraph and no recharge pause: commits to short straight bursts back-to-back, re-aiming only briefly (~0.12s) between bursts. Relentless but dodgeable by moving perpendicular (it overshoots and must re-aim).
- Reuses
dash_timer/dash_phase(RUSH_AIM vs RUSH_BURST) — no new columns. - Per-type tunables from the bible (
rush_speed,rush_burst_s,rush_aim_s). - Carried by a new enemy archetype (see §4c) and/or applied to an existing fast type; spawn-gated past the 10s baseline window → determinism-safe.
4. Enemies — variation, escorts, new archetypes, visuals
Section titled “4. Enemies — variation, escorts, new archetypes, visuals”4a. Per-enemy attribute variation (re-pins baseline)
Section titled “4a. Per-enemy attribute variation (re-pins baseline)”- In
_spawn_enemies(survival) andStoryDirectorrandom-replay spawns (NOT tutorial-authored encounters), each spawned enemy gets correlated jitter drawn fromrng:- one “size roll”
s ∈ [0.8, 1.25]scales radius; bigger → +HP, −speed, +contact, smaller → −HP, +speed, −contact (a believable size/mass link), - plus small independent noise on HP and speed,
- an occasional stronger “variant” roll (~8%) pushes one stat further (a glass-cannon sprinter or a mini-tank).
- one “size roll”
- Helper
EnemyPool.add(...)already takes explicit radius/hp/speed/contact, so variation is applied at the call site — no pool changes. - Determinism: draws from
rnginside the baseline window and mutatesenemies.data/pos→ moves the checksum; re-pin.
4b. Tank-escort rule (survival) (re-pins baseline)
Section titled “4b. Tank-escort rule (survival) (re-pins baseline)”- In
_spawn_enemies, whenpick_typereturnsTYPE_TANKorTYPE_BRUTE, also spawn a small escort cluster (e.g. 3–5 swarmers/spiders) near the heavy so it never arrives alone. (Story already does this in Warden rooms.) - Determinism: extra
rngdraws + spawns → re-pin (batched with §4a).
4c. New weapon-mirroring archetypes (spawn-gated → determinism-safe)
Section titled “4c. New weapon-mirroring archetypes (spawn-gated → determinism-safe)”Five new enemies, each echoing a player weapon, with distinct speed. They attack
via the enemy_proj pool, which gains a per-projectile damage column
(currently every enemy shot deals the uniform SHOOTER_PROJ_DAMAGE); plus two
new mechanics (a telegraphed beam line, a delayed AoE).
| Enemy | Mirrors | Speed | Attack |
|---|---|---|---|
| Zapper | pulse (lightning) | fast | quick single bolts at the player |
| Bomber | nova (fire) | slow | lobs a delayed AoE blast (telegraph ring → burst) |
| Orbiter | orbit (cold) | medium | cold shards orbiting it (contact hazard) |
| Lancer | beam (light) | slow/ranged | telegraphed straight beam-line (line-vs-player) |
| Scatterer | scatter (blood) | medium | fan of pellets |
- New
EnemyPool.TYPE_*ids (continue fromTYPE_BRUTE=8),_build_enemy_typesresize, bible entries (element + stats + behavior),SpawnDirector.pick_typebands (gated ≥ ~30–60s so the <10s baseline is untouched),main.gdrender LUT resized to the new max id, and per-archetype shapes (§4d). enemy_projdamagecolumn swap-removes in lockstep;_check_player_hituses per-shot damage instead of the constant. Determinism:enemy_projis empty in the baseline (no shooters in <10s), so adding the column + per-shot damage is byte-identical.
4d. Visual archetype differentiation (render-only, determinism-neutral)
Section titled “4d. Visual archetype differentiation (render-only, determinism-neutral)”- Replace the single-mesh swarm with per-archetype silhouette meshes: build a
small set of
MultiMeshInstance2Dpartitions, one per shape, and route each enemy into its shape bucket each frame by atype_id → shapeLUT. Keeps the one-draw-call-per-shape property (~10 shapes = cheap) and preserves the additive halo layer. - Shapes: triangle (swarmer), heavy hexagon (tank/brute), diamond (shooter), chevron (skirmisher), spiky star (elite/dasher), asterisk (spider), + distinct shapes for the five new archetypes (e.g. zapper = jagged bolt, bomber = round with fuse, orbiter = ringed dot, lancer = long sliver, scatterer = burst).
- Headless tests assert: total instance count across buckets ==
enemies.count, and a giventype_idlands in its shape bucket. Pixel shapes verified by playtest (the MultiMesh headless-readback caveat). - Update the public site legend (
~/Claude/bullet-heaven-site/index.htmlEnemies section) to match the new silhouettes/colours (project rule).
5. New boss — 5 attacks, random each cycle (time-gated → determinism-safe)
Section titled “5. New boss — 5 attacks, random each cycle (time-gated → determinism-safe)”A second boss entity alongside the existing 4-attack Warden. Lives in the
EnemyPool (weapons damage it for free). A new BossState variant (or a
boss_kind flag) selects one of five attacks at random each cycle (rng),
with longer telegraphs on the two heaviest. Survival alternates the two bosses;
story Warden rooms may assign either.
- Cutter (beam) — locks a long-range beam toward the player; the sweep
rotates slow, then accelerates right before it cuts (the damaging beam
fires along the line for a brief window). Telegraphed rotating line; reuses the
FxManagerbeam node + a line-vs-player hit check. - Artillery (rockets + shrapnel) — fast rockets that arc up and away
(render: rising + shrinking to fake altitude, off the top of the view), then
land at semi-random points around the player after a short hang, each with
a ground target ring (telegraph) → impact → radial shrapnel burst.
Dedicated
boss_rocketsstructure with phases ascend → hang → land → shrapnel (shrapnel viaenemy_proj). The “3D” look is a pure render scale trick; the sim only tracks a 2D landing point + timers. - Shockwave rings — concentric expanding rings of shots with weave-gaps.
- Charge slam (dash) — telegraphs a line, then dashes hard across the arena dealing heavy path damage (ties into the dodge/thruster theme).
- Summon swarm — spawns a cluster of adds to pressure the player.
- New render needs: rocket ascend/descend + ground-target ring + shrapnel; the
accelerating beam telegraph. Both reuse existing FX building blocks where
possible. Every new entity pool/
fx_eventskind must have a renderer/matcharm (the “invisible entity” rule). - HUD boss bar reuses
boss_render_info()(already readsboss.max_hp).
6. Balance pass + damage numbers
Section titled “6. Balance pass + damage numbers”6a. Damage numbers (determinism-neutral — fx_events excluded from checksum)
Section titled “6a. Damage numbers (determinism-neutral — fx_events excluded from checksum)”DMGNUM_MIN7.0 → 2.0 (ordinary hits now show),DMGNUM_BIG18 → 14,DMGNUM_CAP18 → 28/tick. Big/reaction hits still flash gold.
6b. Armor model (determinism-safe — armor-0 enemies unaffected)
Section titled “6b. Armor model (determinism-safe — armor-0 enemies unaffected)”_damage_enemyflooramount * 0.1→amount * 0.25: chip/ranged weapons do at least 25% of their hit to armored foes (today beam 0.4 vs armor 8 ≈ 0.04 — effectively useless). Baseline swarmers are armor-0, so the floor branch never changes for them → byte-identical.
6c. Weapon base damage (non-blade = determinism-safe; blade re-pins)
Section titled “6c. Weapon base damage (non-blade = determinism-safe; blade re-pins)”- Baseline is blade-only, so tuning every other weapon is free. Proposed (playtest-final): pulse 2→3, orbit 0.8→1.2, beam 0.4→0.8, nova 3→4, turret 0.7→1.0, scatter 1.0→1.4. Blade stays 3.0 to avoid churning the baseline (revisit only if play demands it, accepting a re-pin).
6d. XP, gold, meta cost
Section titled “6d. XP, gold, meta cost”- XP curve (
xp_to_next *= 1.35): if changed, re-pins (xp/level are in the checksum). Proposal: keep 1.35 unless play shows level-ups dry up; treat any change as a batched re-pin. - Gold (determinism-neutral —
run_goldnot in checksum):BOSS_GOLD25→40; heavy/ranged kills (tank/brute/elite/skirmisher + new archetypes) drop a small bonus (+2–3) so gold tracks effort.GOLD_PER_KILLstays 1. - Meta cost (render-side
MetaState, determinism-neutral): cap geometric growth at 1.6 (haste/bulwark 1.7 and swiftness 1.8 are steep); keep base costs. - Telemetry note (optional): the dashboard
avg_dpsis ~2,000,000 — almost certainly total-damage-not-per-second or a bad divisor ingameplay_telemetry. Worth a fix so future balance is data-driven, but out of this spec’s core scope.
7. Determinism plan
Section titled “7. Determinism plan”Baseline (survival, seed 1234, 600 ticks, blade-only):
snapshot_string().hash() = 4152236597, state_checksum() = 1267954985
(pinned in tests/test_determinism_checksum.gd).
| Change | Baseline impact |
|---|---|
| Modes/tutorial, damage-number thresholds, visual shapes | neutral |
| Player thruster, rusher behavior | neutral (no dash input / gated spawns) |
New archetypes + enemy_proj damage column |
neutral (gated; pool empty in baseline) |
| New boss (all 5 attacks) | neutral (time-gated) |
| Armor floor 0.1→0.25 | neutral (baseline enemies armor-0) |
| Non-blade weapon damage, gold, meta cost | neutral |
| Per-enemy variation (§4a) | re-pin |
| Tank escorts (§4b) | re-pin |
| Blade dmg / XP curve (if changed) | re-pin |
The re-pinning changes are batched and the baseline re-pinned once at the end
of that group, with the new checksums recorded in
tests/test_determinism_checksum.gd and CLAUDE.md.
8. Chunking & sequencing
Section titled “8. Chunking & sequencing”Each chunk follows the bh-dev-chunk ritual (TDD → import → boot-check →
test-count → determinism → commit → tvOS sync) and bumps Sim_Const.BUILD +
the site changelog when deployed. Determinism-neutral chunks first; the
baseline-movers grouped last with a single re-pin.
- Damage numbers (§6a) — quick win.
- Modes & tutorial separation (§2).
- Visual archetype differentiation (§4d) + site legend.
- Player thruster + rusher dash (§3) — designed together.
- New weapon-mirroring archetypes (§4c) — may split: (a)
enemy_projdamage column + Zapper/Scatterer/Bomber, (b) Orbiter + Lancer (beam-line/orbit shards). - New 5-attack boss (§5) — may split: (a) state machine + Cutter + Charge + Summon, (b) Artillery rockets/shrapnel + Shockwave rings + render.
- Armor + non-blade weapon + gold + meta-cost balance (§6b–6d, neutral parts).
- Per-enemy variation + tank escorts (§4a/4b) + any blade/XP balance — the batched re-pin chunk (last).
9. Risks & mitigations
Section titled “9. Risks & mitigations”- Render perf (pillar = thousands @ 60fps): per-shape MultiMesh adds ~a few draw calls; profile if shape count climbs. New boss/enemy FX gated to events.
- Invisible-entity trap: every new pool (
boss_rockets, orbit shards) and every newfx_eventskind needs a renderer /matcharm — audited per chunk. - tvOS input for the thruster: limited Siri-remote buttons; pick one free of
the decoy/confirm bindings; verify on device via
bh-deploy. - Balance is reasoned, not measured: ship behind playtest; once the gameplay telemetry accumulates (and the DPS metric is fixed) re-tune from real data.
- Re-pin honesty: never silence the determinism test — re-pin with recorded new checksums and a note on what moved them.