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).
Chris’s calls (2026-10-01)
Section titled “Chris’s calls (2026-10-01)”- “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.
Global constraints
Section titled “Global constraints”/simstays pure; determinism rules in CLAUDE.md apply. A baseline move is allowed only for a mechanic that genuinely changes sim behaviour, and only through skillbh-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. Followdocs/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 infx/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.godotandaudio/bus_layout.tres— restore both before committing. - Shader edits: compile-check windowed under
mobileANDgl_compatibility. - Run single GUT files with
-gselect=<name>; full gate isscripts/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)”- Generalise the Pulse switch:
DevFlags.CANDIDATE_LOOKS := {"pulse": [...existing...], "beam": ["v1", <slug for A>, <B>, <C>, <D>], "railgun": [...same shape...]}and a per-widcandidate_lookdictionary, persisted. Keep reading the oldpulse_lookjson key so a saved Pulse choice survives. Keep whitelist validation and the non-dev-build-forces-off rule. FxManager: route"beam"and"railgun_lance"events byev["wid"]throughv2_look()exactly like"bolt"(legacy_spawn_beamwhen no look).set_candidate_look("railgun", slug)also loadscandidates/railgun/<slug>/mark.gdintomark_layer(freeing the previous one;v1clears it).main.gd: re-apply every saved candidate look afterfx_layeris rebuilt in_new_run;apply_dev_command("weapon_look")persists for any wid inCANDIDATE_LOOKS;caps/flags output exposes the lists as letters for the console.- Debug Settings: one cycle row per weapon (“Pulse Look”, “Beam Look”, “Railgun Look”), labels
v1/A-Dfor Beam and Railgun. Pulse keeps its current labels (already de-blinded by Chris). - Remote Playtest Console (
control/src/panel.html.js): Beam and Railgun selectors next to Pulse, showing letters; updatecontrol/test/panel.test.mjs. - 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. - Verify by eye:
tools/weapon_vfx/ingame.gdwithWEAPON=beam|railgun CANDIDATE=<slug>(bothEVOLVE=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”- Add a wait mode to
tools/weapon_vfx/ingame.gdfor persistent zones (WEAPON=wildfire EVOLVE=1: poll untilwildfire.burn_zones()is non-empty;thunder_wakelikewise) with a dense horde so a 4-deep chain happens. Capture at 1920×1080 with the normal horde/effects running. - 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_detailcheap. Prefer the existing additive/blend_addneon convention so bloom carries it. - Before/after captures for both zones saved to the SDD workspace; update the vfx_library manifest
rating/notes for
wildfire_zoneandthunder_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.
- Slewing beam:
WeaponBeamkeeps an aim direction per weapon; each tick it turns toward the current nearest target by at mostturn_rate * dtand fires along the aim. First acquisition snaps. Tunablesbeam_turn_rate= 6.0 rad/s,beam_lance_turn_rate= 3.5 rad/s (evolved);0= snap (today’s behaviour, exactly). - Hit count: the
beamfx event carries"hits"= enemies damaged by that shot. - Lance cadence: evolved Lance fires every
beam_lance_cooldown_s= 0.25 s atbeam_lance_damage_mult= 2.5× per hit (same DPS).0.1/1.0= today. - Tests: turn-rate cap, snap on first acquisition,
0reproduces 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)”- Target distance:
railgun_lancefx event carries"target_dist"= distance to the farthest aimed target. - Hit-confirm on marked enemies (A2 = B1): new
EnemyPoolfloat columnlast_hit_time(simrun_time), written inElementalSystem.damage_enemy, swap-remove safe, NOT part of any determinism checksum.FxManager.draw_markspasses 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). - Charge tell: when the Railgun cooldown crosses
railgun_tell_s= 0.1 s remaining, latch the aim direction and emit arailgun_tellfx 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 implementspawn_eventfor it later. - Mark detonation: when a mark expires, or a marked enemy dies, it pops for
railgun_mark_pop_frac= 0.25 × lance damage to enemies withinrailgun_mark_pop_radius= 40 px. Respect the deferred-death sweep: deaths found in_sweep_deadqueue the pop; pops are resolved at the next Railgun update, never mid-sweep. Emit amark_popfx event with a legacy render consumer (small light-coloured ring).0frac = off. - VFX library manifest entries + scores for
railgun_tellandmark_pop; RELEASE-1.0.1 item for the mechanics batch. - Tests per mechanic incl. off-value equivalence and the deferred pop ordering.
Not in this plan
Section titled “Not in this plan”- 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.
Finish
Section titled “Finish”Gate green, branch pushed, then a dev install to Chris’s Apple TV so he can play the candidates.
Execution handoff
Section titled “Execution handoff”- Execution mode: subagent-driven (skill
superpowers:subagent-driven-development+bh-sdd-concurrent), implementers =bh-implementerwithmodel: "opus", task reviews on Sonnet. One task at a time. Ledger:.superpowers/sdd/2026-10-01-fx-v2-batch2-ingame-and-mechanics/progress.mdin the worktree. - Work ONLY in the worktree
/Users/chris/Claude/bullet-heaven/.claude/worktrees/fx-v2-batch2. The main checkout has another session’s uncommittedCLAUDE.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 main222a264d(console tabbed panel + Debug Settings parity, which reshapedui/debug_settings_panel.gdandmain.gdcaps). The pre-rebase copy is still on origin under the same name; force-push is confirm-first, so push the rebased branch asorigin fx-v2-batch2-ingameinstead. - State at handoff: the previous plan (
2026-10-01-weapon-fx-v2-batch2.md) is complete; boards andblind-sheet.mdwere 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 viabh-determinism-repinif 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=;.uidfiles are gitignored; candidate looks must be registered after_on_mode_chosen;scripts/bump-build.shis the only BUILD writer. - Next action: dispatch Task 1.