public/js/render/fx.js becomes a re-export barrel. Speeds, camera heights, fonts, the kind table and the shared tunables move verbatim into public/js/render/fx/*.js. FxSystem keeps its constructor, clear, update, counts and destroy; the other methods move verbatim into container classes installed on FxSystem.prototype in source order. simSpeeds and fontsReady each stay one instance. The dynamic import of /sim/constants.js stays next to setSimProjectileSpeeds.
105 lines
4.3 KiB
JavaScript
105 lines
4.3 KiB
JavaScript
// public/js/render/fx/locks.js — FxSystem lock reticles.
|
|
// Installed on FxSystem.prototype by ./system.js (a method container: never instantiated; `this` is the effect system).
|
|
|
|
import { SHOT_HEIGHT, bodyZ } from './camera.js';
|
|
import { FX_KINDS } from './kinds.js';
|
|
import { easeOut } from './limits.js';
|
|
|
|
/**
|
|
* Lock reticle: how long (game s) a lock outlives the last sign of its shooter's S3 going on — its latest lock, shell or
|
|
* bombard (a lock whose shell / shot never comes) — its fade-out (real s), and the most reticles kept at once (a long
|
|
* S3: ammo grants / reloads lock far more than the 5 base shots, ≤ 33 shells).
|
|
*/
|
|
const LOCK_T = 5, LOCK_FADE = 0.2, MAX_LOCKS = 48;
|
|
|
|
export class FxLocks {
|
|
// ---- lock-on reticles (蕾缪安) -------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* fx 'lock': a reticle on the locked enemy (following its view; at the last spot once it is gone) until its shell
|
|
* lands (mortar / _landed) or the aimed shot fires ('crit' from the same shooter); at most LOCK_T game seconds after
|
|
* the shooter's last lock / shell / bombard (_touchLocks).
|
|
*/
|
|
_lock(view, src, x, y, z) {
|
|
let L = this.lockFree.pop();
|
|
if (!L) {
|
|
const P = this.P;
|
|
const ring = new P.Sprite(this.tex.reticle);
|
|
ring.anchor.set(0.5);
|
|
ring.blendMode = P.BLEND_MODES.ADD;
|
|
const core = new P.Sprite(this.tex.glow);
|
|
core.anchor.set(0.5);
|
|
core.blendMode = P.BLEND_MODES.ADD;
|
|
this.projLayer.addChild(ring, core);
|
|
L = { ring, core };
|
|
}
|
|
L.view = view; L.id = view ? view.id : null; L.src = src ?? null; L.x = x; L.y = y; L.z = z;
|
|
L.t = 0; L.idle = 0; L.max = LOCK_T / this._ts(); L.out = -1; L.shell = false; L.sx = x; L.sy = y;
|
|
L.ring.tint = L.core.tint = FX_KINDS.lock.c;
|
|
L.ring.alpha = L.core.alpha = 0;
|
|
L.ring.visible = L.core.visible = true;
|
|
this.locks.push(L);
|
|
if (this.locks.length > MAX_LOCKS) this._freeLock(this.locks.shift());
|
|
return L;
|
|
}
|
|
|
|
/** Shooter `src` is still at it (a new lock, a shell, a bombard): its live locks restart their LOCK_T wait. */
|
|
_touchLocks(src) {
|
|
if (src == null) return;
|
|
for (const L of this.locks) if (L.src === src) L.idle = 0;
|
|
}
|
|
|
|
/** The live lock of shooter `src` nearest to (x, y) within `within` tiles (`free`: only one no shell took yet). */
|
|
_nearestLock(src, x, y, within, free) {
|
|
let best = null, bd = Infinity;
|
|
for (const L of this.locks) {
|
|
if (L.out >= 0 || (free && L.shell) || (src != null && L.src != null && L.src !== src)) continue;
|
|
const d = Math.hypot(L.x - x, L.y - y);
|
|
if (d < bd - 1e-9) { bd = d; best = L; }
|
|
}
|
|
return best && bd <= within ? best : null;
|
|
}
|
|
|
|
/** Fade a lock out (it pops outwards as it goes). */
|
|
_releaseLock(L) { if (L && L.out < 0) L.out = 0; }
|
|
|
|
_freeLock(L) {
|
|
L.ring.visible = L.core.visible = false;
|
|
L.view = null;
|
|
this.lockFree.push(L);
|
|
}
|
|
|
|
_updateLocks(dt) {
|
|
if (!this.locks.length) return;
|
|
const cam = this.ctx.cam();
|
|
let w = 0;
|
|
for (const L of this.locks) {
|
|
L.t += dt;
|
|
L.idle += dt;
|
|
if (L.out < 0 && L.idle >= L.max) L.out = 0;
|
|
// the shooter knocked out / withdrawn: the sim fires no more shells — only a shell already in the air still lands
|
|
if (L.out < 0 && !L.shell && L.src != null && this.ctx.view) {
|
|
const sv = this._viewOf(L.src);
|
|
if (!sv || sv.alive === false) L.out = 0;
|
|
}
|
|
if (L.out >= 0) { L.out += dt; if (L.out >= LOCK_FADE) { this._freeLock(L); continue; } }
|
|
const v = L.view;
|
|
if (v && !v.destroyed && v.alive !== false) { L.x = v.x; L.y = v.y; L.z = bodyZ(cam, v, SHOT_HEIGHT.aim); }
|
|
const p = cam.project(L.x, L.y, L.z, this._p);
|
|
const s = p.s;
|
|
const out = L.out >= 0 ? L.out / LOCK_FADE : 0;
|
|
const pop = L.t < 0.16 ? 1.7 - 0.7 * easeOut(L.t / 0.16) : 1 + 0.05 * Math.sin(L.t * 9);
|
|
const a = (L.out >= 0 ? 1 - out : Math.min(1, L.t / 0.06));
|
|
L.ring.position.set(p.x, p.y);
|
|
L.ring.rotation = L.t * 1.8;
|
|
L.ring.scale.set((s * 0.95 * pop * (1 + out * 0.6)) / 128);
|
|
L.ring.alpha = 0.95 * a;
|
|
L.core.position.set(p.x, p.y);
|
|
L.core.scale.set(((s * 0.4) / 128) * (1 + 0.25 * Math.sin(L.t * 14)));
|
|
L.core.alpha = 0.7 * a;
|
|
this.locks[w++] = L;
|
|
}
|
|
this.locks.length = w;
|
|
}
|
|
}
|