Files
ark-stronghold-protocol/public/js/render/fx/system.js
T

175 lines
8.3 KiB
JavaScript

// 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","officialNumber","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));
}