PURPOSE
Boss VFX layer kit — composable, additively-stacked visual layers for boss encounters. Provides persistent host/arena-anchored layers (auras, core pulses, under-shadows, orbital rings, rune circles, lattice/conduit lines, dust motes, floor glow stripes, hex tile shimmer, refraction sweeps), screen-space overlays (tint pulse, edge vignette, fullscreen slice), telegraph holds (cone/circle/line/bloom), and one-shot bursts (shockwaves, sparks, shards, beam charges, projectile trails, glass cracks, ground impacts) for boss data files to compose without re-implementing rendering primitives.
OWNS
- Module-private
layers: BossLayer[]array — every active persistent layer, transient telegraph, and screen-space overlay lives here untilkill()or natural lifetime expiry. MAX_LAYERS = 512hard cap (crashes on overflow) andSOFT_LAYER_WARN = 256soft threshold (one-shot console warn per session).- Per-pass ms accumulators
_passMsfor the boss-gauntlet telemetry sampler:bossVfxUnder,bossVfxAdditive,tickBossVfxLayers,tickAllAbilities. - Easing curve table (
applyEase) supportinglinear,easeOutCubic,easeOutQuart,easeOutBack,easeOutElastic,easeInExpo,easeInOutCubic,pulseSine. - Tween machinery for alpha, radius, and linear color interpolation on each layer.
- Hex color parsing/format helpers (
parseHex,rgba) that crash on malformed input — colors come from data tables, not user input. - The
LayerHandlereturned to callers for retuning alpha, color, radius, rotate-rate, pulse-rate, and killing layers (with optional fade-out window). - Lazy-seeded mote particle field on
dustMoteslayers (count derived fromdensity, capped at 40).
READS FROM
engine/core— globalcamera, screenW,H.engine/rendering/camera—Camera.toS,Camera.toSx,Camera.zoomPulsefor world-to-screen projection and the genuine fire-and-forget camera zoom pulse.engine/core/config—PERF_FLAGS.lowPerfviaisLowPerfMode().engine/core/types—EnemyEntityandBossArenashapes for host/arena anchors.engine/vfx/particles—Particles.addfor spark/shard/beam-charge/projectile-trail emission.engine/vfx/sonar-rings—SonarRings.shockwaveandSonarRings.shockwaveArcfor shockwave bands, shockwave stacks, telegraph blooms, and hit-glass-crack arcs.
PUSHES TO
- The active Canvas 2D context via
renderBossVfxLayersUnder(source-over pass for under-shadow / dust motes / floor glow stripes / refraction sweep / ground impact / telegraph circle) andrenderBossVfxLayersAdditive(lighter pass for aura / core pulse / orbital ring / rune circle / lattice line / conduit line / hex tile shimmer / telegraph cone / telegraph line, followed by screen-space overlays usingoverlay,source-over, andlightercomp-ops per kind). - The
Particlessystem (sparks, shards, beam-charge sparks, projectile trail dots). - The
SonarRingssystem (single rings, staggered stacks, partial-arc cracks). - The
Cameraprimitive (zoomPulse) for screen-shake-adjacent zoom kicks.
DOES NOT
- Does not advance gameplay or AI — visual decay uses wall-dt (
rawDt), cooldown/AI logic stays on game-dt elsewhere. - Does not own boss data, palette tables, or per-boss
setupVfxorchestration — callers compose layers from their own data files and readisLowPerfMode()to decide which ambient layers to skip. - Does not draw the boss sprite itself —
renderBossVfxLayersUnderandrenderBossVfxLayersAdditiveare wrapped around the sprite render by the caller. - Does not handle audio, damage, hit detection, telegraph→fire resolution, or any gameplay consequence — purely visual.
- Does not silently truncate when the layer cap is hit —
pushLayerthrows to surface caller leaks loudly. - Does not provide smooth tween ramps for
setRotateHz/setPulseHz— these snap-set the rate; easing/duration params are forward-compat hooks only. - Does not persist across encounters —
clearBossVfxLayersis the teardown helper. - Does not auto-clean dead-host layers —
latticeLineandconduitLineskip rendering when either host’saliveflag flips false but stay in the layer array until killed.
Signals
- Throws
boss-layers: layer cap exceeded (512) — caller leaked persistent layerswhenpushLayerwould exceed the hard cap. - Throws
boss-layers: malformed color '<hex>', expected '#RRGGBB'whenparseHexrejects a color string. - One-shot
console.warn:boss-layers: live layer count crossed 256 (cap 512) — investigate density / leaksthe first time the layer count crosses the soft threshold in a session.
Entry points
isLowPerfMode(): boolean— read by per-bosssetupVfxto skip ambient layers (dust motes, floor glow stripes, refraction sweep) and per-swarm-member core pulses on low-perf devices.tickBossVfxLayers(rawDt: number): void— advances every layer’s age, resolves in-flight tweens, expires transient layers, and splices dead entries. Wall-dt only.renderBossVfxLayersUnder(ctx: CanvasRenderingContext2D): void— source-over pass painted under the boss sprite.renderBossVfxLayersAdditive(ctx: CanvasRenderingContext2D): void— lighter pass painted over the boss sprite, plus screen-space overlay tail.createVfxLayerKit(): VfxLayerKit— returns the public factory bag of methods every boss data file uses to compose layers (aura,corePulse,underShadow,orbitalRing,runeCircle,latticeLine,conduitLine,dustMotes,floorGlowStripes,hexTileShimmer,refractionSweep,telegraphBloom,telegraphCone,telegraphCircle,telegraphLine,shockwaveBand,shockwaveStack,additiveSparkBurst,spiralSparkBurst,shardsBurst,beamCharge,projectileTrail,hitGlassCrack,groundImpactCircle,screenTintPulse,edgeVignette,fullscreenSlice,cameraZoomPulse).clearBossVfxLayers(): void— encounter-teardown helper that empties the layer array.bossVfxLayerCount(): number— live layer count for debug overlays and perf checks.getBossVfxPassMs()— latest per-pass ms breakdown for the gauntlet runner’s telemetry sampler.accumulateAbilityTickMs(deltaMs: number): void— bridge hook for the tick loop to feedtickAllAbilitiesms into the per-pass breakdown.- Exported types:
BossPalette,Easing,LayerHandle,VfxLayerKit.
Pattern notes
- Two-pass render:
renderBossVfxLayersUnder(source-over) for floor effects,renderBossVfxLayersAdditive(lighter) for glow stacks, with screen-space overlays appended at the tail of the additive pass using per-kind comp-ops (overlayfor screen tint,source-overfor edge vignette,lighterfor fullscreen slice). - Wall-dt decay is invariant — every visual ages off
rawDtso death cinematics still play through pause/slow-mo while cooldown/AI logic continues on game-dt elsewhere. - Persistent layers attach to an
EnemyEntityhost, a secondhostB(lattice/conduit endpoints), aBossArena, or a static world position; render-timeanchorX/anchorYfalls through host → arena center →x/y→ 0. - Transient layers (telegraphs, screen overlays, ground impacts) set
lifetimeat creation and auto-expire intickBossVfxLayers;kill(fadeOutDuration)retro-fits a lifetime by tweening alpha to 0 over the window. LayerHandleis the only contract callers hold onto a layer — screen-space layers (screenTintPulse,edgeVignette,fullscreenSlice) return handles specifically so phase-transition code can.kill()long-duration overlays (e.g.edgeVignetteatduration=9999s) early and avoid stack leaks;cameraZoomPulseis the only fire-and-forget call (routes throughCamera.zoomPulse).setRotateHzandsetPulseHzsnap-set the underlying rate channels and mirror across paired fields (rotateHz/sweepHz, pulseHz/freqHz) so the call is uniform regardless of layer kind; easing/duration params are deliberately unused — implementing a smooth ramp would phase-jump rotation since the renderers computeage * ratedirectly.dustMoteslazily seeds its particle field on first draw (count =min(40, max(4, density * 60))), uses a fixed 0.016s sub-step (frame-time spikes lose a tiny amount of motion), and wraps motes that drift past the arena radius with a –0.5x velocity reflection.refractionSweeppaints four bands (primary/secondary/warning/cool) across the arena along a rotating sweep axis, clipped to the arena footprint.floorGlowStripesclips to the arena footprint and scrolls perpendicular to a fixed axis atscrollHzcycles per stripe period.- Telegraph renderers (
drawTelegraphCone,drawTelegraphCircle,drawTelegraphLine) all share the same alpha curve:easeInExporamp-in for the full lifetime, plus a 4Hz alpha pulse multiplier during the final 30% as a fire-soon strobe. fullscreenSlicefades in over the first 10% of lifetime, holds, then 8Hz alpha-pulses during the final 30%.corePulsefalls back to a default radius of 8 world units when the caller passes 0/undefined; per-boss data files override (Digbot Swarm usesradius=4to keep dots tight on small swarm bodies).- Color tweens interpolate linearly on parsed RGB channels and snap to the target color when
t >= 1. - Tween records mutate layer state inside
resolveTweenonly when the tween completes — in-flight resolution is a pure read. MAX_LAYERS = 512was raised from 256 after Digbot Swarm’s worst case (~25 sampled core pulses + 4 Foreman markers + 2 arena layers + transient bursts during a phase-transition cinematic) broke the original cap;SOFT_LAYER_WARN = 256preserves the old cap as a leak/density warning._perfNowusesperformance.now()when available, falls back toDate.now()—performance.mark()was rejected because it would build an unbounded entry buffer the gauntlet runner would have to clear every sample.latticeLine’sadditiveparameter is preserved in the API but ignored — lattice lines always render in the additive pass per spec; the flag is forward-compat only.parseHexandpushLayercrash loudly on bad data — no silent fallbacks, in line with the codebase rule that internal config errors must surface immediately.