Skip to content

Weapon FX v2 batch 2: Beam + Railgun blind shootout, Railgun mark, Thunder Wake + Firestorm zones — design

Approved by Chris 2026-10-01. Parent spec: 2026-09-28-weapon-fx-v2-design.md (house rules, scaling model, element motifs — all still apply). Branch fx-v2-batch2-beam-railgun.

  • Beam and Railgun are the weakest weapons on the VFX board (tools/vfx_library/library_manifest.gd, both rated 2). They share one legacy path, FxManager._BeamNode via the "beam", "railgun_lance" handler (fx/fx_manager.gd:463), which reads only pos, dir, length, element.
  • Bug 2 of the parent spec is still open: the handler ignores width, power and evolved, so Beam Lance (width ×2.5) and Prism Lance look identical to the base weapons. (docs/RELEASE-1.0.1.md :46-48 wrongly says batch 1 fixed it — batch 1 only fixed the slash arcs; corrected by this batch.)
  • Three player-facing mechanics have no renderer (red MISSING cards): the Railgun mark (enemies.mark_timer/mark_amt, +20% damage from every source, 0.6 s, compounding with the mark mod), Thunder Wake puddles (WeaponThunderWake._zones) and Firestorm zones (WeaponWildfire._burn_zones). Both zone arrays live on the weapon instance, not sim.zones, so ZoneRenderer never sees them.

Scope change vs the parent spec: Thunder Wake + Firestorm zones move from batch 3 into batch 2 (Chris’s call — cheap plumbing, closes two MISSING cards). The Thunder Wake and Wildfire weapon looks stay in batch 3.

Format (Chris’s call): four models each build one Beam look and one Railgun look — Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 4.5. Eight candidates total, each a separate agent in its own worktree, under the existing contract tools/weapon_vfx/CANDIDATE_BRIEF.md.

  • Artistic licence, explicitly including mechanics. Candidates may reinterpret what the effect is and are invited to propose weapon mechanic changes that would make the VFX better. Proposals are written in CANDIDATE.md (and, if the look depends on one, prototyped behind a const inside the candidate folder) — never by editing /sim. Proposals are reviewed by Chris as a separate list after the pick; none ship by default.
  • Blind. Slugs are model names; the board shows letters only. Letters are shuffled independently per weapon so a model’s style can’t be tracked across the two boards. Mapping in fx/weapons/candidates/<wid>/_labels.json, not opened until the pick is made.
  • Staggered: Beam’s four first, then Railgun’s four, so windowed captures don’t queue on scripts/dev/godot-window-lock.sh.
  • Judged on the shootout board (p0 + evolved, plus a rung row). The in-game live switch from the Pulse shootout is uncommitted on pulse-shootout; generalising it now would collide, so it is out of scope for this round.

The mark is Railgun’s payoff, so it is part of the Railgun look, not a separate job. Each Railgun candidate additionally delivers mark.gd + mark.gdshader + mark_mat.tres in its folder:

  • mark.gd extends WeaponFxLayer (immediate mode: begin/put/commit), no class_name, and exposes func draw_marks(enemies: EnemyPool, now: float) -> void, called once per rendered frame. One quad per enemy with mark_timer > 0; mark_timer drives the fade-out, mark_amt must make a stacked mark visibly stronger than a single one. Tint is the weapon’s light identity colour.
  • It must read as a target tell at a glance without whiting out a dense swarm (BASE_DIM budget rules from the brief apply). It is a second draw call; acceptable.
  • A Railgun candidate with no mark fails the brief.

Part B — prep (single author, before any candidate is dispatched)

Section titled “Part B — prep (single author, before any candidate is dispatched)”
  1. Generalise tools/weapon_vfx/shootout.gd beyond Pulse: per-weapon event synthesis with the real event fields (pos, dir, length, width, element, plus the WeaponVfx.stamp fields wid, power, evolved, rungs), using each weapon’s real sim consts and evolve multipliers for width and length; the v1 baseline column drawn through the real _BeamNode; Railgun draws two lances when evolved (Prism Lance forces lance_count ≥ 2). Pulse must keep working unchanged.
  2. Mark scene in the shootout: a row of dummy enemies with mark states (fresh single, fading single, stacked) driving each candidate’s draw_marks.
  3. Mark hook in the live game, dormant until a winner exists: the per-frame call site (render side, read-only on sim.enemies) that will drive the promoted mark layer.
  4. Brief addendum for this batch: the mark contract above, the mechanic-proposal invitation, and the per-weapon notes (Beam: fast 0.1 s cooldown continuous-ish beam, light, rungs width/reach; Railgun: farthest-target piercing lance, light, rungs mark/lances(aether — motif not built, render as no-op)).

Part C — zones (single author, in parallel with the shootout)

Section titled “Part C — zones (single author, in parallel with the shootout)”
  • Thunder Wake puddles: ZoneRenderer reads each pilot’s Thunder Wake via its existing (currently uncalled) zones() accessor and draws a lightning-tinted disc fading on remaining / duration, in the same style as sim.zones.
  • Firestorm zones: add a read-only burn_zones() getter to sim/weapon_wildfire.gd (the only /sim edit in this batch — no RNG draw, no state, no checksum effect) and draw a fire-tinted disc with a slow ember flicker so it doesn’t read as a Supernova remnant crater.
  • Neither zone carries an element field; the tint comes from the weapon’s element.

COPY each winner into fx/weapons/ with a class_name, wire into V2_LOOKS, remove from PENDING_V2_LOOKS (tests/test_weapon_fx_guards.gd), dispatch beam/railgun_lance through the look (legacy _BeamNode stays as the v2_active() fallback), add the look + mark pipelines to PrewarmPass family 12, and update manifest entries/scores. Every candidate folder, the winner’s included, stays in fx/weapons/candidates/ as submitted (superseded 2026-10-01: Chris’s standing policy is that the candidates are a permanent archive, never deleted, so a future model’s take can be compared against all of them in-game; see fx/weapons/candidates/README.md). Mechanic proposals go to Chris as a list; any approved one is its own follow-up change with a determinism check.

  • scripts/gate.sh green on every candidate worktree and on the merged branch.
  • Every new or edited shader compile-checked in a windowed run under BOTH --rendering-method mobile and --rendering-method gl_compatibility; project.godot restored after.
  • Prep and zones are TDD (GUT): shootout event shapes per weapon (width/evolved actually differ), mark hook draws one instance per marked enemy and none for unmarked, a live puddle / Firestorm zone produces a draw, burn_zones() returns the live zones. Determinism baselines must be unchanged.
  • VFX library: railgun_mark, thunder_wake_zone, wildfire_zone move from MISSING to CURRENT with real scores; tests/test_vfx_library_manifest.gd pins them.

Pulse (own shootout on pulse-shootout), the Thunder Wake / Wildfire weapon looks (batch 3), building the aether/poison/time motifs, the in-game candidate switch, enemy element reactions (phase 6).