explosion-fx.ts

PURPOSE

Layered explosion VFX for enemy deaths, bullet impacts, and AoE detonations. Exports two singletons: ExplosionFX (pre-baked stamp explosions with weapon-colored tinting plus expanding halo damage rings) and AoeExplosion (vector-shape explosion for missile/mortar/cannon detonations combining sonar pulse, boundary ring, and 4-layer fireball core). All rendering uses additive blending for natural bloom stacking. Stamps and tint canvas are baked lazily on first draw and cached forever.

OWNS

  • entities[] — pool of ExplosionEntity (cap MAX_EXPLOSIONS = 40) for stamp-based explosions.
  • _haloRings[] — pool of HaloRingEntity (cap MAX_HALO_RINGS = 30) for expanding damage rings.
  • _aoeEntities[] — pool of AoeExplosionEntity (cap MAX_AOE = 20) for vector AoE explosions.
  • stampCacheRecord<string, HTMLCanvasElement> holding the four baked 128×128 stamps (core, rays, ring, debris).
  • _tintCv / _tintCx — single shared 128×128 offscreen canvas reused for source-atop color multiply on white stamps.
  • Layer-spec presets DEATH_LAYERS, IMPACT_LAYERS, BIG_LAYERS defining stamp, delay, duration, fade ramps, scale curve, peak alpha, and rotation per layer.
  • Fireball palette FIREBALL_LAYERS (4 layers: 255/100/30 → 255/136/0 → 255/204/0 → white) with radiusFrac, alphaMax, and per-layer stagger.
  • Timing constants: FB_EXPAND_END, FB_HOLD_END, FB_TOTAL, FB_SHRINK_TO, SONAR_DURATION, SONAR_RADIUS_MULT, BOUNDARY_DURATION, BOUNDARY_RAMP_FRAC, and derived AOE_MAX_TIMER.
  • Stamp size constants STAMP_SZ = 128 and STAMP_HALF.

READS FROM

  • ../corecamera (for camera.zoom), W, H (screen dimensions for frustum culling).
  • ../rendering/cameraCamera.toS(x, y) to project world coordinates into screen space.
  • ../core/configPERF_FLAGS imported (not currently referenced in any branch).
  • document.createElement('canvas') and 2D contexts for stamp baking and tint scratch surface.
  • Math.random() for per-explosion rotationOffset so explosions do not look identical.

PUSHES TO

  • The CanvasRenderingContext2D passed into ExplosionFX.draw(ctx) / AoeExplosion.draw(ctx) — sets globalCompositeOperation = 'lighter', globalAlpha, fillStyle, strokeStyle, lineWidth, plus save/translate/rotate/restore and drawImage/arc/fill/stroke calls. Both draw methods restore globalAlpha = 1 and globalCompositeOperation = 'source-over' before exiting.
  • The internal _tintCv canvas — each _tintStamp call clears it, draws the white stamp, then overlays solid RGB via source-atop. The returned canvas reference is shared, so the caller must draw it before the next tint call overwrites it.

DOES NOT

  • Does not deal damage. haloRing is a visual-only effect — actual AoE damage is applied by the caller (collision-resolver). Despite the source comment mentioning “hits enemies once as it passes through”, this file only renders the ring.
  • Does not own world simulation state — explosions are purely cosmetic timers, removed when timer >= maxTimer.
  • Does not respond to pause directly — it relies on the caller passing dt = 0 while paused.
  • Does not allocate per-frame: stamps are baked once, the tint canvas is reused, entity arrays use swap-remove for O(1) deletion.
  • Does not handle off-screen entities efficiently beyond frustum culling at draw time (update still ticks all entries).
  • Does not consult PERF_FLAGS despite importing it; thermal scaling of fireballLayers is set up structurally (the field exists on AoeExplosionEntity) but is always assigned FIREBALL_LAYERS.length at spawn.
  • Does not bake stamps until the first draw() call — _ensureStamps and _ensureTintCanvas are lazy.
  • Does not validate colorHex strictly — hexToRgb falls back to 128 for any channel that fails to parse.

Signals

  • ExplosionFX.death(wx, wy, radius, colorHex) — death explosion (3 layers: core, rays, ring; ~0.22s).
  • ExplosionFX.impact(wx, wy, radius, colorHex) — bullet impact (2 layers: core, ring; ~0.14s).
  • ExplosionFX.big(wx, wy, radius, colorHex) — large/explosive (3 layers: core, rays, ring; ~0.33s).
  • ExplosionFX.haloRing(wx, wy, maxRadius, colorHex, durationSec=0.3, thickness=15) — expanding ring visual.
  • ExplosionFX.update(dt) — advances all stamp explosion and halo ring timers, removes expired.
  • ExplosionFX.draw(ctx) — renders all stamp explosions then halo rings.
  • ExplosionFX.clear() — empties entities and _haloRings.
  • ExplosionFX.count — getter returning entities.length + _haloRings.length.
  • AoeExplosion.spawn(wx, wy, blastR, colorHex) — vector AoE explosion (sonar + boundary + fireball).
  • AoeExplosion.update(dt) — advances AoE timers, swap-removes expired.
  • AoeExplosion.draw(ctx) — renders fireball, boundary ring, sonar pulse for each entity.
  • AoeExplosion.clear() — empties _aoeEntities.
  • AoeExplosion.count — getter returning _aoeEntities.length.

Entry points

  • damage.ts spawns death explosions on enemy death.
  • collision-resolver spawns impact explosions on bullet hit and AoeExplosion.spawn when explosive or homing bullets detonate.
  • bridge.ts calls ExplosionFX.update(dt) and ExplosionFX.draw(ctx) each frame; the same bridge drives AoeExplosion.update / AoeExplosion.draw.
  • Run reset paths call ExplosionFX.clear() and AoeExplosion.clear().

Pattern notes

  • Stamp baking mirrors SmokeFX._bakeStamp. Four shapes: core (radial gradient white→amber→transparent), rays (12 tapered triangles clipped from a radial gradient), ring (hollow ring via stacked gradient stops), debris (24 dots scattered using golden-angle spacing of 137.508°).
  • Tint via shared offscreen canvas + source-atop. _tintStamp clears the scratch canvas, draws the white stamp, then fillRects the target RGB with source-atop so the color only affects existing pixels. Returns the shared canvas — must be drawn before the next call.
  • Three-phase per-layer alpha curve. Within each layer, frac = layerT / duration; alpha ramps linearly from 0 to alphaMax over fadeIn, holds at alphaMax, then ramps back to 0 over fadeOut. Alpha below 0.01 short-circuits the draw.
  • Scale interpolation is linear between scaleStart and scaleEnd; drawRadius = radius * scale * camera.zoom, skipped if < 1 screen pixel.
  • Frustum culling at draw time uses radius * 3 * camera.zoom for stamp explosions and blastR * SONAR_RADIUS_MULT * camera.zoom for AoE (since the sonar pulse is the widest layer at 1.5× blastR).
  • Rotation is optional. If rot === 0 the draw uses the cheap drawImage(x, y, w, h) path; otherwise it wraps in save/translate/rotate/restore.
  • Swap-remove pattern in both update methods: iterate backwards, on expiry overwrite slot i with the last element and shrink length by 1 — O(1) deletion without preserving order.
  • Cap-and-drop spawn policy. New explosions are silently dropped when the entity array hits its cap; no overwrite of oldest.
  • AoE fireball uses sqrt easing during expansion (radius = maxR * sqrt(frac), alpha = alphaMax * sqrt(frac)) for fast initial bloom that decelerates, then a hold phase, then linear shrink to FB_SHRINK_TO = 0.60 of max while alpha fades linearly.
  • AoE boundary ring uses cubic ease-out (1 - (1-frac)^3) so radius reaches exact blastR smoothly; alpha ramps over the first BOUNDARY_RAMP_FRAC = 0.15 then fades linearly. Stroke width scales with zoom but is clamped to >= 1 pixel.
  • Sonar pulse uses quartic ease-out (1 - (1-frac)^4) for near-instant expansion to 1.5× blastR, with quadratic alpha falloff 0.7 * (1-frac)^2. Fixed white color and lineWidth = 2.5 (not zoom-scaled).
  • Layer 0 of the fireball uses the weapon’s identity color; layers 1-3 use the fixed orange/yellow/white palette regardless of the weapon.
  • Random rotation offset per explosion breaks visual repetition without per-frame randomness.
  • Minimum sizes enforced at spawnradius clamped to >= 8 for stamp explosions, blastR clamped to >= 20 for AoE, maxRadius clamped to >= 10 for halo rings.
  • Halo rings draw after stamp explosions in ExplosionFX.draw so the ring stroke sits on top of the stamp bloom.