PURPOSE
Particle system for sparks, smoke, exhaust, and star confetti. Ported from the legacy 05-vfx.js Particles module. Owns the pooled Particle object, the public Particles singleton (singular spawn, omnidirectional burst, hex-color burst, muzzle flash, warp arrival poof, star burst, impact burst), the per-frame integrator, and the supporting overlay accumulators (Notifications, RewardFeed, DmgNumbers, XpAccum, ShieldDmgAccum, HpDmgAccum) that live alongside it in the same file.
OWNS
Particleinterface —x, y, vx, vy, l, ml, sz, r, g, b, a, tp(tp is'spark' | 'smoke' | 'exhaust' | 'star').- Module-local particle pool (
_pool) and thePOOL_MAX = 600cap. _acquire()/_release(p)— pool checkout/return; allocates a fresh particle only when the pool is empty.Particlessingleton:update(dt),add(x, y, vx, vy, l, sz, r, g, b, a=1, tp='spark'),burst(cx, cy, n, tp, col, sp=80),_hexToRgb(hex),burstHex(cx, cy, n, tp, hex, sp=80),muzzleFlash(cx, cy, fireAngle, n, hex1, hex2?),warpArrivalPoof(cx, cy, hex, radius),starBurst(wx, wy, count, lifetime),impactBurst(cx, cy, n, hex1, hex2?).DMG_ARC_GRAVITY = 200(px/s²) — gravity for damage-number arc physics.- Sibling overlays exported from the same file:
Notifications,RewardFeed,DmgNumbers,XpAccum,ShieldDmgAccum,HpDmgAccum.
READS FROM
world.particles— the live particle array driven byupdateand gated byCFG.MAX_PARTICLES.world.dmgNumbers— read and written byDmgNumbers.CFG.MAX_PARTICLESfrom../core— hard cap onworld.particles.lengthenforced by every spawn path.PERF_FLAGSfrom../core/config— imported (currently used by adjacent VFX wiring; available to spawn paths).swapRemovefrom../core/utils— O(1) removal of dead particles and dead damage numbers.globalThis.ship— read byXpAccum.updateto emit aqua absorption sparks above the ship.globalThis.AIControl— read byRewardFeed.addto suppress text when AI control is enabled.
PUSHES TO
world.particles— pushed byParticles.addafter pool acquisition; popped/released viaswapRemoveinupdatewhenl <= 0.world.dmgNumbers— pushed byDmgNumbers.add,DmgNumbers.addLabel,DmgNumbers.addText; removed viaswapRemovewhenl <= 0.- The internal
_poolarray — receives released particles up toPOOL_MAX. Notifications.e,RewardFeed.e— internal text-event arrays drained by their ownupdatemethods.
DOES NOT
- Does not render. No draw calls, no canvas/WebGL access — rendering lives in the VFX/render layer that reads
world.particlesandworld.dmgNumbers. - Does not own time scaling.
updateaccepts whateverdtthe caller hands it (notes onstarBurstconfirmbridge.tspassesrawDtso star confetti animates through time freeze). - Does not despawn beyond lifetime expiry; there is no distance/visibility culling here.
- Does not modify particle color or alpha over lifetime — colors are set at spawn; visual fade is the renderer’s responsibility using
l / ml. - Does not apply drag to
sparkorstarparticles; drag (0.92^(dt*60)) is applied only to'smoke'and'exhaust'. - Does not allocate when the pool is warm —
_acquirepops from_poolbefore constructing a new object. - Does not gate by
PERF_FLAGSitself —PERF_FLAGSis imported but spawn caps come fromCFG.MAX_PARTICLESand the soft-budget filter (see Pattern notes).
Signals
- Hard cap: every spawn path checks
world.particles.length < CFG.MAX_PARTICLESbefore pushing; over-cap requests are silently dropped. - Soft cap:
Particles.addadditionally drops'smoke'and'spark'requests once the array is at or above 80% ofCFG.MAX_PARTICLES, preserving headroom for more important spawns (impact, muzzle, star, exhaust). DmgNumbers.addenforcesDMG_CAP = 15for damage numbers (separate from particle cap);addLabelandaddTextuse a higher 80-entry cap onworld.dmgNumbers.Notifications.addkeeps at most 4 entries (shifts oldest);RewardFeed.addkeeps at most 3 entries.XpAccum/ShieldDmgAccum/HpDmgAccumare singletons withactiveflag,total,refreshes,timer,rise,age, and amaxLife(2.5s for XP, 1.2s for shield/HP).
Entry points
Particles.update(dt)— called once per frame; integrates position, decays lifetime, applies drag to smoke/exhaust, releases dead particles back to the pool.Particles.add(...)— low-level singular spawn used byburst,muzzleFlash,warpArrivalPoof,starBurst,impactBurst, and external callers (e.g.XpAccum).Particles.burst(cx, cy, n, tp, col, sp=80)— radial RGB burst; per-particle speed40 + rand*sp, lifetime0.1 + rand*0.25, size2 + rand*2for smoke or1 + rand*1.5otherwise, alpha0.9.Particles.burstHex(cx, cy, n, tp, hex, sp=80)— hex-color wrapper overburst.Particles.muzzleFlash(cx, cy, fireAngle, n, hex1, hex2?)— 70% of particles in a±0.75 radforward cone, 30% omnidirectional; speed60 + rand*120, lifetime0.08 + rand*0.15, alpha0.85, type'spark', color picked 60/40 betweenhex1andhex2.Particles.warpArrivalPoof(cx, cy, hex, radius)— sparks (min(8, 4 + floor(radius*0.2))) plus smoke (min(4, 2 + floor(radius*0.1))); offset from center by±radius*0.6(sparks) /±radius*0.4(smoke).Particles.starBurst(wx, wy, count, lifetime)— golden five-pointed-star confetti at RGB(255, 200, 50), alpha0.95, type'star'; used for weapon chest pickup, drives through time freeze viarawDt.Particles.impactBurst(cx, cy, n, hex1, hex2?)— bullet-impact starburst; speed30 + rand*90, lifetime0.1 + rand*0.2, alpha0.9, type'spark'.Notifications.add/update,RewardFeed.add/update,DmgNumbers.add/addLabel/addText/update,XpAccum.collect/update,ShieldDmgAccum.collect/update,HpDmgAccum.collect/update— sibling overlay APIs called from gameplay code and ticked from the main loop.
Pattern notes
- Pool-then-cap.
_acquirereuses pooled objects to avoid GC pressure; thePOOL_MAX = 600ceiling is the recycle bin size, separate from the live capCFG.MAX_PARTICLES = 400. Released particles past the bin size are dropped to GC. - Soft-priority filter in
add. The 80% threshold blocks'smoke'and'spark'spawns first, leaving budget for'exhaust'/'star'/'impact'spawns that bypass the soft filter (those types calladdonly after their own cap check). - Backwards iteration with
swapRemove. BothParticles.updateandDmgNumbers.updateiteratei = length-1down to0and remove viaswapRemovefor O(1) deletes without disturbing earlier indices. - Drag is frame-rate-independent.
Math.pow(0.92, dt * 60)keeps the smoke/exhaust deceleration constant in real time regardless of frame rate. - Damage-number staging.
DmgNumbers.addstamps a random 1-3 frame_delay;updatedecrements_delaybefore ticking lifetime, spreading AoE bursts across frames so numbers don’t all appear in one tick. - Damage-number arc-then-freeze. Enemy damage numbers integrate
vxand gravity-affectedvyfor the first0.25sof life, then lock position and fade out in place — punchier read than continuous motion. Player block-text damage (block: true) uses the older pause-then-rise behavior instead. - Auto-color by magnitude.
DmgNumbers.addpicks color and base size from damage value when no explicitcolis provided: white<100, yellow>=100, orange>=400, red>=1000; crits force#ffcc44at 1.3x size; final size is scaled*1.4for visibility through dark backgrounds. - Accumulator pattern.
XpAccum,ShieldDmgAccum,HpDmgAccumcollapse rapid pickup/hit events into a single stacking popup keyed offactive+timer;XpAccumalso emits aqua absorption sparks from the ship at a rate that scales withrefreshes. AIControlsuppression.RewardFeed.addis the only path that consultsglobalThis.AIControlto skip text emission when AI control is engaged.- Globals via
globalThis.shipandAIControlare accessed throughglobalThis as anyrather than imported, reflecting the legacy bridge-time wiring inherited from the JS port.