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
PostFxTypeunion:streak,scar,ghost_arc,exhaust,flutter,impact_ring,muzzle_triangle.- Module-private
entitiesarray ofPostFxEntityrecords (world coords start/end, RGB color, timer, maxTimer). MAX_FXcap (80) —spawnis dropped silently when full.hexToRgbprivate helper that parses#rrggbbstrings (falls back to 128 per channel on parse failure).PostFxpublic object withspawn,update,draw,clear, and acountgetter.- Per-type render logic for each
PostFxTypeinsidedraw.
READS FROM
Camera.toSfrom../rendering/camerato convert world coords to screen coords each draw.camera.zoomfrom../coreto scale line widths, radii, triangle sizes, and arc jitter.- The 2D canvas context passed to
draw. Math.random()forghost_arcjitter and spark scatter.
PUSHES TO
- The provided
CanvasRenderingContext2Donly — strokes, fills, gradients, arcs, paths. UsesglobalCompositeOperation = 'lighter'for additive blending across all post-FX in a frame, wrapped insave/restore. - Mutates its own
entitiesarray viapush(spawn) andsplice(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
flashtype 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. countgetter 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). Forimpact_ring,x2is repurposed as start radius andy2as 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 perfx.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
updateenables in-placesplicewithout index drift. - Each FX entry tracks
timerandmaxTimerto derive normalized fadet = 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 below0.01. - All render passes share a single
save/restoreand a singleglobalCompositeOperation = 'lighter'for additive blending. - Line widths and radii scale with
camera.zoomand clamp viaMath.maxto keep features visible at low zoom. streakshrinks from the tail toward the impact point via ashrink = t * 0.6lerp on the start coord, with a linear gradient from transparent at the new start to opaque at impact.scarholds full length and fades in place; its alpha is halved relative to base.ghost_arcrenders 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.exhaustis a single fading circle at the start point that grows slightly witht.fluttertwinkles viasin(t * PI * 8)at the segment midpoint and gates rendering when the computed radius drops below 0.3.muzzle_trianglebuilds 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. Usesctx.globalAlpha(1.4x and 1.6x of base alpha) for over-bright bloom — restored by the surroundingctx.restore().impact_ringrepurposesx2/y2as start/end radii; ring radius lerps from start to end across lifetime and scales with zoom; ring alpha is0.4 * (1 - t).- Comment header lists
weapons.tsas the spawn consumer andbridge.tsas the per-frameupdateanddrawdriver.