PURPOSE

Cosmetic-only aftereffects that persist briefly after weapon impact or fire. Purely visual: no collision, no damage, no entity hooks. Keeps gameplay readable while making weapons feel richer.

OWNS

  • PostFxType union: streak, scar, ghost_arc, exhaust, flutter, impact_ring, muzzle_triangle.
  • Module-private entities array of PostFxEntity records (world coords start/end, RGB color, timer, maxTimer).
  • MAX_FX cap (80) — spawn is dropped silently when full.
  • hexToRgb private helper that parses #rrggbb strings (falls back to 128 per channel on parse failure).
  • PostFx public object with spawn, update, draw, clear, and a count getter.
  • Per-type render logic for each PostFxType inside draw.

READS FROM

  • Camera.toS from ../rendering/camera to convert world coords to screen coords each draw.
  • camera.zoom from ../core to scale line widths, radii, triangle sizes, and arc jitter.
  • The 2D canvas context passed to draw.
  • Math.random() for ghost_arc jitter and spark scatter.

PUSHES TO

  • The provided CanvasRenderingContext2D only — strokes, fills, gradients, arcs, paths. Uses globalCompositeOperation = 'lighter' for additive blending across all post-FX in a frame, wrapped in save/restore.
  • Mutates its own entities array via push (spawn) and splice (expiry).
  • No store, no telemetry, no audio, no Supabase, no DOM outside the canvas.

DOES NOT

  • Does not implement a full-screen flash effect — there is no flash type and no full-viewport rectangle fill.
  • Does not implement a nausea or motion-sickness cap, intensity throttle, or per-user reduction setting.
  • Does not run collision checks, deal damage, or spawn gameplay entities.
  • Does not own bullets, beams, missiles, or chain logic — only their cosmetic linger.
  • Does not own particle systems, smoke, player glow, or explosion FX (those live in sibling modules in engine/vfx).
  • Does not persist state across runs — clear() resets to empty.
  • Does not impose a per-type cap; only the shared MAX_FX = 80.
  • Does not pool entities; expired entries are spliced.

Signals

  • No event bus, no observers. Communication is direct function calls into PostFx.
  • count getter is the only outbound read surface (debug-only).

Entry points

  • PostFx.spawn(type, x1, y1, x2, y2, colorHex, lifetime) — append one FX entry (no-op when at cap). For impact_ring, x2 is repurposed as start radius and y2 as end radius.
  • PostFx.update(dt) — decrement timers, splice expired entries (iterates backwards).
  • PostFx.draw(ctx) — render all active FX additively. Early-returns when empty. Dispatches per fx.type.
  • PostFx.clear() — empty the entities array on run reset.
  • PostFx.count — current entity count.

Pattern notes

  • Singleton object literal exported as PostFx; state is module-level, not instanced.
  • Backward iteration in update enables in-place splice without index drift.
  • Each FX entry tracks timer and maxTimer to derive normalized fade t = 1 - timer / maxTimer (0 at spawn, 1 at expiry).
  • Base alpha curve is 0.6 * (1 - t), clamped at 0; entries skip rendering when alpha drops below 0.01.
  • All render passes share a single save/restore and a single globalCompositeOperation = 'lighter' for additive blending.
  • Line widths and radii scale with camera.zoom and clamp via Math.max to keep features visible at low zoom.
  • streak shrinks from the tail toward the impact point via a shrink = t * 0.6 lerp on the start coord, with a linear gradient from transparent at the new start to opaque at impact.
  • scar holds full length and fades in place; its alpha is halved relative to base.
  • ghost_arc renders three passes: thick outer color glow, bright white core, and four scattered blue spark dots. Jitter magnitude grows with (1 - t), so the arc is most chaotic at spawn and stabilizes as it fades. Step count is derived from world-space length divided by 8.
  • exhaust is a single fading circle at the start point that grows slightly with t.
  • flutter twinkles via sin(t * PI * 8) at the segment midpoint and gates rendering when the computed radius drops below 0.3.
  • muzzle_triangle builds a perpendicular basis from the fire-direction vector, offsets the base center slightly forward, and draws an outer color triangle plus a narrower, shorter white-hot inner triangle. Skipped when length is below 1. Uses ctx.globalAlpha (1.4x and 1.6x of base alpha) for over-bright bloom — restored by the surrounding ctx.restore().
  • impact_ring repurposes x2/y2 as start/end radii; ring radius lerps from start to end across lifetime and scales with zoom; ring alpha is 0.4 * (1 - t).
  • Comment header lists weapons.ts as the spawn consumer and bridge.ts as the per-frame update and draw driver.