Skip to content

FX v2 batch 2 — play the candidates in-game, Firestorm visibility, approved mechanics

FX v2 batch 2 follow-up: in-game candidates, Firestorm visibility, mechanics

Section titled “FX v2 batch 2 follow-up: in-game candidates, Firestorm visibility, mechanics”

Branch fx-v2-batch2-beam-railgun (worktree .claude/worktrees/fx-v2-batch2). Follows 2026-10-01-weapon-fx-v2-batch2.md (all 7 tasks done, boards delivered).

  • “wire all of these into the game the same way we did for [the Pulse]” — the bench boards don’t show how a look reads in a busy fight. Every Beam and Railgun candidate (and each Railgun mark) must be switchable live from Debug Settings, exactly like DevFlags.pulse_look (commit bf9ba9da).
  • “make sure the Firestorm zone is actually visible” — verify in real play, among other effects.
  • “I quite like all the proposals including the full game determinism change ones” — every mechanic proposal on the blind sheet is approved.
  • /sim stays pure; determinism rules in CLAUDE.md apply. A baseline move is allowed only for a mechanic that genuinely changes sim behaviour, and only through skill bh-determinism-repin (the seed-1234 baselines do not equip Beam or Railgun, so a move is itself a red flag to explain).
  • Every mechanic is a remote-config tunable whose “off” value restores today’s behaviour exactly (memory bullet-heaven-toggle-competing-techniques: feel comparisons need a switch). Defaults are ON at the values below. Follow docs/architecture/remote-config.md § “Converting a const to a tunable”.
  • The Debug Settings rows stay BLIND: show v1, A, B, C, D — never a model slug. The letter is the index into a DevFlags list ordered by the letters in fx/weapons/candidates/<wid>/_labels.json. Slugs may appear in code/json (persisted value), never in any player/dev-visible text.
  • Any new sim state that changes damage/targeting needs a render consumer in the same task (CLAUDE.md).
  • Windowed runs dirty project.godot and audio/bus_layout.tres — restore both before committing.
  • Shader edits: compile-check windowed under mobile AND gl_compatibility.
  • Run single GUT files with -gselect=<name>; full gate is scripts/gate.sh.

Task 1 — Live candidate switching for Beam and Railgun (look + mark)

Section titled “Task 1 — Live candidate switching for Beam and Railgun (look + mark)”
  1. Generalise the Pulse switch: DevFlags.CANDIDATE_LOOKS := {"pulse": [...existing...], "beam": ["v1", <slug for A>, <B>, <C>, <D>], "railgun": [...same shape...]} and a per-wid candidate_look dictionary, persisted. Keep reading the old pulse_look json key so a saved Pulse choice survives. Keep whitelist validation and the non-dev-build-forces-off rule.
  2. FxManager: route "beam" and "railgun_lance" events by ev["wid"] through v2_look() exactly like "bolt" (legacy _spawn_beam when no look). set_candidate_look("railgun", slug) also loads candidates/railgun/<slug>/mark.gd into mark_layer (freeing the previous one; v1 clears it).
  3. main.gd: re-apply every saved candidate look after fx_layer is rebuilt in _new_run; apply_dev_command("weapon_look") persists for any wid in CANDIDATE_LOOKS; caps/flags output exposes the lists as letters for the console.
  4. Debug Settings: one cycle row per weapon (“Pulse Look”, “Beam Look”, “Railgun Look”), labels v1/A-D for Beam and Railgun. Pulse keeps its current labels (already de-blinded by Chris).
  5. Remote Playtest Console (control/src/panel.html.js): Beam and Railgun selectors next to Pulse, showing letters; update control/test/panel.test.mjs.
  6. Tests: extend tests/test_candidate_look_switch.gd — every offered slug exists for every wid, the railgun swap installs and clears the mark layer, v1 restores legacy for beam/railgun, no slug text in the row labels.
  7. Verify by eye: tools/weapon_vfx/ingame.gd with WEAPON=beam|railgun CANDIDATE=<slug> (both EVOLVE=0/1) produces a frame showing the candidate (not the legacy beam); save captures to the SDD workspace, not the repo.

Task 2 — Firestorm + Thunder Wake zones visible in real play

Section titled “Task 2 — Firestorm + Thunder Wake zones visible in real play”
  1. Add a wait mode to tools/weapon_vfx/ingame.gd for persistent zones (WEAPON=wildfire EVOLVE=1: poll until wildfire.burn_zones() is non-empty; thunder_wake likewise) with a dense horde so a 4-deep chain happens. Capture at 1920×1080 with the normal horde/effects running.
  2. Judge the frame honestly against “can a player see there is a burning zone here, among everything else?” Today’s draw is fill α 0.10, inner α 0.12, 2.5 px rim α 0.55 — expect it to be too faint. Make it unmistakable without hiding enemies: e.g. a stronger animated rim (≥4 px), licking flame tongues or ember flecks drifting up inside, a gentle heat pulse; Thunder Wake gets the equivalent electric treatment. Keep low_detail cheap. Prefer the existing additive/blend_add neon convention so bloom carries it.
  3. Before/after captures for both zones saved to the SDD workspace; update the vfx_library manifest rating/notes for wildfire_zone and thunder_wake_zone.

Task 3 — Beam mechanics (Beam A + B1 + D merged, B2, B3)

Section titled “Task 3 — Beam mechanics (Beam A + B1 + D merged, B2, B3)”

Beam A, B1 and D are three versions of one idea (a beam that turns instead of snapping); they merge into one mechanic.

  1. Slewing beam: WeaponBeam keeps an aim direction per weapon; each tick it turns toward the current nearest target by at most turn_rate * dt and fires along the aim. First acquisition snaps. Tunables beam_turn_rate = 6.0 rad/s, beam_lance_turn_rate = 3.5 rad/s (evolved); 0 = snap (today’s behaviour, exactly).
  2. Hit count: the beam fx event carries "hits" = enemies damaged by that shot.
  3. Lance cadence: evolved Lance fires every beam_lance_cooldown_s = 0.25 s at beam_lance_damage_mult = 2.5× per hit (same DPS). 0.1/1.0 = today.
  4. Tests: turn-rate cap, snap on first acquisition, 0 reproduces old direction exactly, hits count, Lance cadence/damage, determinism suite green (explain any baseline move).

Task 4 — Railgun mechanics (A1, A2/B1 merged, A3, C)

Section titled “Task 4 — Railgun mechanics (A1, A2/B1 merged, A3, C)”
  1. Target distance: railgun_lance fx event carries "target_dist" = distance to the farthest aimed target.
  2. Hit-confirm on marked enemies (A2 = B1): new EnemyPool float column last_hit_time (sim run_time), written in ElementalSystem.damage_enemy, swap-remove safe, NOT part of any determinism checksum. FxManager.draw_marks passes it implicitly via the pool; add a small generic flash on a marked enemy hit within the last 0.12 s (FxManager-owned, so every mark candidate gets it).
  3. Charge tell: when the Railgun cooldown crosses railgun_tell_s = 0.1 s remaining, latch the aim direction and emit a railgun_tell fx event (pos, dir, length, element); the shot then fires along the LATCHED direction so the tell never lies. 0 = off (today). Legacy render consumer: a thin fading hairline in FxManager; candidate looks MAY implement spawn_event for it later.
  4. Mark detonation: when a mark expires, or a marked enemy dies, it pops for railgun_mark_pop_frac = 0.25 × lance damage to enemies within railgun_mark_pop_radius = 40 px. Respect the deferred-death sweep: deaths found in _sweep_dead queue the pop; pops are resolved at the next Railgun update, never mid-sweep. Emit a mark_pop fx event with a legacy render consumer (small light-coloured ring). 0 frac = off.
  5. VFX library manifest entries + scores for railgun_tell and mark_pop; RELEASE-1.0.1 item for the mechanics batch.
  6. Tests per mechanic incl. off-value equivalence and the deferred pop ordering.
  • Railgun B2 “rail scar” (0.3 s harmless afterglow) is a look choice with no sim component — it is applied to whichever Railgun look wins, at promotion.
  • Promoting winners, merging to main, store builds.

Gate green, branch pushed, then a dev install to Chris’s Apple TV so he can play the candidates.

  • Execution mode: subagent-driven (skill superpowers:subagent-driven-development + bh-sdd-concurrent), implementers = bh-implementer with model: "opus", task reviews on Sonnet. One task at a time. Ledger: .superpowers/sdd/2026-10-01-fx-v2-batch2-ingame-and-mechanics/progress.md in the worktree.
  • Work ONLY in the worktree /Users/chris/Claude/bullet-heaven/.claude/worktrees/fx-v2-batch2. The main checkout has another session’s uncommitted CLAUDE.md, docs/RELEASE-1.0.1.md, platform/tvos/export_presets.cfg, sim/constants.gd (a BUILD bump in progress there) — never touch it.
  • Branch: work continues on local fx-v2-batch2-beam-railgun, which was REBASED onto main 222a264d (console tabbed panel + Debug Settings parity, which reshaped ui/debug_settings_panel.gd and main.gd caps). The pre-rebase copy is still on origin under the same name; force-push is confirm-first, so push the rebased branch as origin fx-v2-batch2-ingame instead.
  • State at handoff: the previous plan (2026-10-01-weapon-fx-v2-batch2.md) is complete; boards and blind-sheet.md were delivered to Chris.
  • Blindness is load-bearing: Chris has NOT picked Beam/Railgun winners. Never show or say a model slug for Beam/Railgun anywhere Chris sees (UI rows, console, reports, chat). Letters come from fx/weapons/candidates/<wid>/_labels.json.
  • Rulings made while planning: Beam A/B1/D merged into one slewing-beam mechanic (same idea at different rates; D’s 6 / 3.5 rad/s chosen). Railgun A2 and B1 are identical (merged). Railgun B2 (rail scar) is look-only → deferred to promotion. All mechanics ship ON behind tunables whose off value is today’s behaviour, so Chris can A/B them.
  • Authorised: implement + commit on the branch, push it (new name above), dev install to Chris’s Apple TV (Dark Cosmos Dev app via bh-deploy / scripts/dev/install-dev.sh) so he can play the candidates — that is what “wire them into the game the same way” asked for. Determinism re-pin only via bh-determinism-repin if a mechanic genuinely moves a baseline.
  • Needs Chris’s same-day OK: merging to main (main checkout is dirty with another session’s bump), store/TestFlight builds, deploying the Remote Playtest Console Worker (control/), publishing a remote-config patch, promoting any winner, force-pushing.
  • Gotchas: windowed Godot dirties project.godot + audio/bus_layout.tres (restore both); single GUT file = -gselect=; .uid files are gitignored; candidate looks must be registered after _on_mode_chosen; scripts/bump-build.sh is the only BUILD writer.
  • Next action: dispatch Task 1.