// public/js/render/fx/system.js — FxSystem: the pools are built here; the effect methods are installed from the containers. // The containers are installed in source order. A name defined twice is an error. import { fxAtlas } from '../textures.js'; import { ensureDamageFonts } from './fonts.js'; import { MAX_PARTICLES } from './limits.js'; import { FxParticles } from './particles.js'; import { FxProjectiles } from './projectiles.js'; import { FxBeams } from './beams.js'; import { FxLocks } from './locks.js'; import { FxNumbers } from './numbers.js'; import { FxRings } from './rings.js'; import { FxArrivals } from './arrivals.js'; import { FxSim } from './simfx.js'; import { FxZones } from './zones.js'; export class FxSystem { /** * @param {{ P, layers: { groundFx, fxAdd, fxNormal, text, screen }, cam: () => any, view: (id) => any, * heightAt: (r,c)=>number, settings: object, assets: any, timeScale: () => number, * subProfOf: (defId) => string|null, screenSize: () => {width,height}, fieldTop: () => number }} ctx */ constructor(ctx) { const P = ctx.P; this.ctx = ctx; this.P = P; this.atlas = fxAtlas(); this.tex = this.atlas.tex; ensureDamageFonts(); const props = { vertices: true, position: true, rotation: true, uvs: true, tint: true }; this.addPc = new P.ParticleContainer(MAX_PARTICLES.high, props, 512, true); this.addPc.blendMode = P.BLEND_MODES.ADD; this.normPc = new P.ParticleContainer(MAX_PARTICLES.high, props, 512, true); // ground shadows of flying shells / boomerangs: normal blend, under the units and the smoke this.shadowLayer = new P.Container(); ctx.layers.fxNormal.addChild(this.shadowLayer, this.normPc); ctx.layers.fxAdd.addChild(this.addPc); this.parts = []; // active particle records { sp, add, x, y, vx, … } this.freeAdd = []; this.freeNorm = []; // pooled particle records (their sprites stay in the containers) this.projs = []; this.projFree = []; this.projLayer = new P.Container(); ctx.layers.fxAdd.addChild(this.projLayer); this.locks = []; // lock-on reticles { view, id, src, x, y, z, t, idle, max, out, ring, core, shell } this.lockFree = []; this._po = {}; // options of the per-frame emitters (see _o) this.beams = new P.Graphics(); this.beams.blendMode = P.BLEND_MODES.ADD; ctx.layers.fxAdd.addChild(this.beams); this.beamList = []; this.flames = []; // fire jets (炎佑 祛恶之焰) { src, tgt, x, y, z, r, col, t, end, disc, edge, … } this.nums = []; this._numPools = new Map(); // bitmap font → free number records this.auras = new Map(); // unit id → { sprite, ring, t } this.rings = []; // ground rings { sprite, x, y, z, t, dur, r0, r1, tint } this.ringFree = []; this.pops = []; // screen-space pops (bond glyphs, coins) this.zones = []; // persistent ground areas (zone / telegraph fx) this.labels = []; // floating '!' / '+n' labels this.tileFlashes = []; // flashing tile sets (telegraphed boxes) this.tileGfx = new P.Graphics(); this.tileGfx.blendMode = P.BLEND_MODES.ADD; ctx.layers.groundFx.addChild(this.tileGfx); this.tintSprite = new P.Sprite(P.Texture.WHITE); this.tintSprite.alpha = 0; this.tintSprite.blendMode = P.BLEND_MODES.ADD; ctx.layers.screen.addChild(this.tintSprite); this.tintT = 0; this.tintDur = 1; this.tintA = 0; this.vignette = new P.Sprite(ctx.redVignette || P.Texture.EMPTY); this.vignette.alpha = 0; ctx.layers.screen.addChild(this.vignette); this.vigT = 0; this.time = 0; this._p = { x: 0, y: 0, s: 0, depth: 0 }; this._q = { x: 0, y: 0, s: 0, depth: 0 }; this._g = { x: 0, y: 0, s: 0, depth: 0 }; } /** Remove everything (battle reset). */ clear() { for (const p of this.parts) this._freeParticle(p); this.parts.length = 0; for (const pr of this.projs) this._releaseProj(pr); this.projs.length = 0; for (const L of this.locks) this._freeLock(L); this.locks.length = 0; this._slashAt = null; for (const t of this.nums) this._releaseNum(t); this.nums.length = 0; for (const r of this.rings) { r.sp.visible = false; this.ringFree.push(r); } this.rings.length = 0; for (const a of this.auras.values()) a.sp.destroy({ children: true }); this.auras.clear(); for (const p of this.pops) p.c.destroy({ children: true }); this.pops.length = 0; this.beamList.length = 0; this.beams.clear(); for (const F of this.flames) this._freeFlame(F); this.flames.length = 0; this.vigT = 0; this.vignette.alpha = 0; for (const zn of this.zones) this._freeZone(zn); this.zones.length = 0; for (const l of this.labels) l.t.destroy(); this.labels.length = 0; this.tileFlashes.length = 0; this.tileGfx.clear(); this.tintT = 0; this.tintSprite.alpha = 0; } update(dt) { this.time += dt; this._updateParticles(dt); this._updateProjs(dt); this._updateLocks(dt); this._updateBeams(dt); this._updateFlames(dt); this._updateNums(dt); this._updateRings(dt); this._updateAuras(dt); this._updatePops(dt); this._updateZones(dt); this._updateLabels(dt); this._updateTileFlashes(dt); if (this.tintT > 0) { this.tintT = Math.max(0, this.tintT - dt); const size = this.ctx.screenSize(); this.tintSprite.width = size.width; this.tintSprite.height = size.height; this.tintSprite.alpha = Math.sin((this.tintT / this.tintDur) * Math.PI) * this.tintA * 0.35; } else if (this.tintSprite.alpha) this.tintSprite.alpha = 0; if (this.vigT > 0) { this.vigT = Math.max(0, this.vigT - dt); const size = this.ctx.screenSize(); this.vignette.width = size.width; this.vignette.height = size.height; this.vignette.alpha = Math.sin((this.vigT / 0.9) * Math.PI) * 0.55; } else if (this.vignette.alpha) this.vignette.alpha = 0; } get counts() { return { particles: this.parts.length, projectiles: this.projs.length, numbers: this.nums.length, rings: this.rings.length, auras: this.auras.size, locks: this.locks.length, flames: this.flames.length, promotions: this.promotions || 0 }; } destroy() { this.clear(); this.addPc.destroy({ children: true }); this.normPc.destroy({ children: true }); this.projLayer.destroy({ children: true }); this.shadowLayer.destroy({ children: true }); this.lockFree.length = 0; this.projFree.length = 0; this.freeAdd.length = 0; this.freeNorm.length = 0; this.beams.destroy(); this.vignette.destroy(); this.tileGfx.destroy(); this.tintSprite.destroy(); for (const list of this._numPools.values()) for (const t of list) t.text.destroy(); this._numPools.clear(); } } const FX_PARTS = [FxParticles, FxProjectiles, FxBeams, FxLocks, FxNumbers, FxRings, FxArrivals, FxSim, FxZones]; const FX_METHOD_ORDER = ["quality","load","maxParticles","rich","_room","_ts","_groundZ","particle","_o","_freeParticle","_updateParticles","_proj","_onGround","_chest","_bodyPt","burst","attack","_takeProj","_dressProj","_releaseProj","_updateProjs","_shotPoint","_stepShot","_stepBoomerang","_stepMortar","_mote","_puff","_muzzle","_impact","_catch","mortar","_landed","_beam","_updateBeams","_flame","_freeFlame","_updateFlames","_lock","_touchLocks","_nearestLock","_releaseLock","_freeLock","_updateLocks","damage","_slash","heal","number","_growFits","_numSlotPx","_layoutNums","_layoutNumsOnce","_sizeNum","_fadeNum","_takeNum","_releaseNum","_updateNums","ring","_updateRings","skill","_aura","_updateAuras","deploy","promote","death","crateBreak","_viewOf","_where","_point","simFx","explosion","smoke","streak","strike","numberAt","_updateLabels","zone","_freeZone","_updateZones","tileFlash","_updateTileFlashes","flashScreen","snowfall","leak","pop","_updatePops"]; for (const key of FX_METHOD_ORDER) { let found = null; for (const part of FX_PARTS) { if (!Object.prototype.hasOwnProperty.call(part.prototype, key)) continue; if (found) throw new Error(`FxSystem.${key} is defined twice`); found = part; } if (!found) throw new Error(`FxSystem.${key} is missing`); if (Object.prototype.hasOwnProperty.call(FxSystem.prototype, key)) throw new Error(`FxSystem.${key} is defined twice`); Object.defineProperty(FxSystem.prototype, key, Object.getOwnPropertyDescriptor(found.prototype, key)); }