Files
ark-stronghold-protocol/public/js/render/fx/simfx.js
T
Convey123 ffa538c39d feedback7 R22-RENDER: 赤刃明霄陈 S3's sword wave is drawn along its path — the sim sends its position as a chen3Wave fx every 0.12 s, the client draws a 'qi' blade (PR #382 by @Convey123, owner item 12); golden: diy-073 / diy-074 events.fx and fx.chen3Wave only
The 赤霄·天喟 wave is a kit-managed probe (it turns corners and lives as long as the skill), not a snapshot
projectile, and the sim emitted one `dash` at the cast and a `slash` per hit — nothing along the way, so the client
drew no wave. op-chen3.js now emits fx('chen3Wave', { x, y, fromX, fromY, dr, dc, id }) every WAVE_FX_EVERY (0.12 s;
4 ticks), the trail restarting at a corner; render/fx registers chen3Wave → 'qi' (pt: drawn at the event, never on
her) and draws a streak plus a crescent head turned the way it travels. The look is a procedural stand-in, not the
official particle prefab.

Substantially @Convey123's code (PR #382), authored as theirs. Changed on review: the head is projected after
streak() — streak() reuses the effect system's projection scratch, so the PR drew the crescent on the trail's start,
one event behind the wave — and the trail, head and heading share one height; an unused test variable dropped.

Golden: diy-073 (5_diy*=chen3 S3 in two slots) events.fx 508 → 1558, fx.chen3Wave → 1050; diy-074 (6_diy*=chen3 S3)
events.fx 272 → 872, fx.chen3Wave → 600. HP, kills, damage, hooks and snapshot hashes unchanged: battle.fx only
records an event.

Tests: test/content/op_chen3.test.js (+1: the events, their cadence, id, direction, trail start, silence after the
skill) 12/12; test/render/fxchen3wave.test.js (new, 6: the archetype, drawn at the wave not at her, the head on the
wave and turned with it, a sweep joining into one trail, junk input, nothing left after) 6/6; boardart 15/15.
2026-10-08 18:48:53 +08:00

323 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// public/js/render/fx/simfx.js — FxSystem placement of a sim fx event.
// 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 { fxSpec, tilesAround, wallTiles } from './kinds.js';
import { clamp } from './limits.js';
/**
* An fx anchored on a unit (extra.id) is drawn at that unit's rendered position while the event's own (x, y) is within
* this many tiles of it (it happens on the unit); farther away the fx happens at (x, y) — the sim puts the caster in
* `id` of many area / target effects (an 'aoe' ahead of the caster, a 'crit' on the victim, 蕾缪安's 'bombard').
*/
const ANCHOR_SNAP = 0.75;
const num = (v, d) => { const n = typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN; return Number.isFinite(n) ? n : d; };
export class FxSim {
// ---- sim fx (b.ev ['fx', kind, x, y, extra]) -------------------------------------------------------------
/** Unit view of an fx's `id` / `src` / … (null when unknown or gone). */
_viewOf(id) {
if (id == null || !this.ctx.view) return null;
const v = this.ctx.view(id);
return v && !v.destroyed ? v : null;
}
/**
* Where an fx happens: on its anchor unit `ex.id` (at the unit's rendered position, so it sticks to a moving unit)
* when the event's (x, y) is that unit's spot (within ANCHOR_SNAP tiles) or has none; otherwise at (x, y) on the ground
* — the sim often names the caster in `id` of an effect elsewhere ('aoe' at a target or ahead of the caster, 'crit'
* on the victim, 'zone' where a skill lands), which used to be drawn on the caster.
*/
_where(x, y, ex) {
const v = this._viewOf(ex.id);
if (v && Number.isFinite(v.x) && Number.isFinite(v.y)) {
const here = !Number.isFinite(x) || !Number.isFinite(y) || Math.hypot(v.x - x, v.y - y) <= ANCHOR_SNAP;
if (here) return { x: v.x, y: v.y, z: (v.z || 0) + (v.hover || 0), v };
}
return this._point(x, y);
}
/** An fx at the sim position (x, y), on the ground there. */
_point(x, y) {
return { x, y, z: Number.isFinite(x) && Number.isFinite(y) ? this._groundZ(x, y) : 0, v: null };
}
/**
* b.ev 'fx': every kind the sim / content emits has a visual (FX_KINDS archetypes: blast, shell, zone, telegraph,
* heal, sp, shield, shatter, summon, vanish, blink, move, wave, mark, reticle, buff, lift, sleep, crit, dodge,
* counter, dp, coin, crate, down, beam, bolt, strike, volley, pillar, lp, chill, element, flame, qi), except kinds whose
* archetype is 'none' (hitCap: a leader hit cancelled by 限伤 draws nothing); unknown kinds get a generic sparkle. `extra` keys used: id (anchor unit — or the shooter of a `pt` kind), r | radius, dur | duration,
* t (shell flight, game s), src / from / to / target / targets (unit ids), fx, fy / fromX, fromY / tx, ty (positions),
* element, n, scale, kind, tiles, hold ('lock': the reticle waits while the shooter's skill runs), dr / dc (a
* direction in tiles: 'qi' points its blade that way).
*/
simFx(kind, x, y, extra) {
const ex = extra && typeof extra === 'object' ? extra : {};
const spec = fxSpec(kind, ex);
if (spec.a === 'none') return; // an event the screen does not show (hitCap)
const at = spec.pt ? this._point(Number(x), Number(y)) : this._where(Number(x), Number(y), ex);
if (!Number.isFinite(at.x) || !Number.isFinite(at.y)) return;
// 蕾缪安 S2: the aimed snipe ('crit' on the locked enemy, from her) ends that aim lock
if (kind === 'crit' && ex.src != null && ex.id != null) {
for (const L of this.locks) if (L.src === ex.src && L.id === ex.id) this._releaseLock(L);
}
const col = spec.c;
const r = clamp(num(ex.r ?? ex.radius, spec.r ?? 1), 0.3, 30);
const ts = this.ctx.timeScale ? Math.max(0.25, this.ctx.timeScale()) : 2;
const dur = num(ex.dur ?? ex.duration, spec.dur ?? 0) / ts;
const cam = this.ctx.cam();
const chest = (v, out = this._p) => (v ? this._chest(v, out) : cam.project(at.x, at.y, at.z + 0.5, out));
const p = chest(at.v);
const s = p.s;
switch (spec.a) {
case 'blast': {
if (r >= 12) { this.flashScreen(col, 0.5); break; }
if (kind === 'bombard') { this._touchLocks(ex.id ?? ex.src ?? null); this._landed(ex.id ?? ex.src ?? null, at.x, at.y, r); }
this.explosion(at.x, at.y, at.z, r, col, { smoke: spec.smoke, heavy: !!spec.heavy, tiles: ex.tiles ? tilesAround(at.x, at.y, r, ex.tiles) : null });
break;
}
case 'shell': {
// 蕾缪安 S3: a shell fired now by `id` that lands at (x, y) after `t` game seconds (its 'bombard' explodes there)
const flight = num(ex.t ?? ex.flight ?? ex.dur ?? ex.duration, 1) / ts;
this._touchLocks(ex.id ?? ex.src ?? null);
this.mortar(this._viewOf(ex.id ?? ex.src), at.x, at.y, r, flight);
break;
}
// `key`: one zone the sim re-sends as it moves / grows (an update in place, never a second layer — zones.js)
case 'zone': this.zone(at.x, at.y, at.z, r, col, Math.max(0.6, dur || 1.5), spec.tex, false, ex.key != null ? String(ex.key) : null); break;
case 'wall': {
// a line of burning tiles through the anchor tile along `axis` ('col' | 'row', from the sim event)
const rect = this.ctx.fieldRect ? this.ctx.fieldRect() : null;
this.tileFlash(wallTiles(Number(x), Number(y), ex.axis === 'row' ? 'row' : 'col', rect), col, Math.max(0.6, dur || 1.5));
this.zone(at.x, at.y, at.z, 0.6, col, Math.max(0.6, dur || 1.5), spec.tex);
break;
}
case 'telegraph': {
const d = Math.max(0.4, dur || 1);
if (r >= 12) { this.flashScreen(col, 0.45, d); break; }
if (ex.tiles) this.tileFlash(tilesAround(at.x, at.y, r, ex.tiles), col, d, true);
else this.zone(at.x, at.y, at.z, r, col, d, 'ring', true);
break;
}
case 'chill': this.flashScreen(col, 0.35, 0.8); this.snowfall(col); break;
case 'heal': this.heal(at.v || { x: at.x, y: at.y, z: at.z }, 0); break;
case 'healAoe': {
this.ring(at.x, at.y, at.z, 0.2, r, col, 0.6);
const n = this.quality === 'low' ? 4 : 9;
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2, d = Math.random() * r * 0.85;
const q = cam.project(at.x + Math.cos(a) * d, at.y + Math.sin(a) * d, at.z + 0.2);
this.particle('plus', q.x, q.y, { tint: col, vy: -q.s * 0.9, life: 0.8, s0: q.s / 64 * 0.3, s1: q.s / 64 * 0.18, a0: 0.9, a1: 0, fadeIn: 0.1 });
}
break;
}
case 'sp': {
for (let i = 0; i < (this.quality === 'low' ? 3 : 7); i++) {
this.particle('dot', p.x + (Math.random() - 0.5) * s * 0.6, p.y + (Math.random() - 0.2) * s * 0.4, { tint: col, vy: -s * (0.6 + Math.random() * 0.6), life: 0.7, s0: s / 32 * 0.2, s1: 0, a0: 1, a1: 0, fadeIn: 0.05 });
}
this.particle('glow', p.x, p.y, { tint: col, life: 0.3, s0: s / 128 * 0.6, s1: s / 128 * 1.1, a0: 0.7, a1: 0 });
break;
}
case 'shield': {
this.particle('hex', p.x, p.y, { tint: col, life: 0.55, s0: s / 128 * 0.9, s1: s / 128 * 1.25, a0: 0.85, a1: 0, sx: 0.85 });
this.particle('glow', p.x, p.y, { tint: col, life: 0.4, s0: s / 128 * 0.9, s1: s / 128 * 1.3, a0: 0.5, a1: 0 });
break;
}
case 'shatter': {
this.particle('hex', p.x, p.y, { tint: col, life: 0.25, s0: s / 128 * 1.1, s1: s / 128 * 1.5, a0: 0.9, a1: 0 });
for (let i = 0; i < (this.quality === 'low' ? 5 : 12); i++) {
const a = Math.random() * Math.PI * 2;
this.particle('shard', p.x + Math.cos(a) * s * 0.3, p.y + Math.sin(a) * s * 0.3, { tint: col, vx: Math.cos(a) * s * 1.6, vy: Math.sin(a) * s * 1.2 - s * 0.3, g: s * 3, life: 0.55, s0: s / 32 * 0.16, s1: s / 32 * 0.05, a0: 1, a1: 0, spin: (Math.random() - 0.5) * 12 });
}
break;
}
case 'summon': {
const g = cam.project(at.x, at.y, at.z);
this.particle('pillar', g.x, g.y, { tint: col, life: 0.55, s0: g.s / 64 * 0.8, s1: g.s / 64 * 0.3, a0: 0.9, a1: 0, sx: 1, ay: 1 });
this.ring(at.x, at.y, at.z, 0.1, 0.9, col, 0.5, 'hex');
this.burst(g.x, g.y - g.s * 0.3, g.s, 8, col, { speed: 1.4, up: 0.8, life: 0.5, tex: 'dot' });
break;
}
case 'vanish': {
this.particle('glow', p.x, p.y, { tint: col, life: 0.4, s0: s / 128 * 0.7, s1: s / 128 * 1.5, a0: 0.8, a1: 0 });
this.smoke(p.x, p.y, s * 0.7, 0x2c2436, 0.5);
this.burst(p.x, p.y, s, 7, col, { speed: 1.5, tex: 'dot', life: 0.45 });
break;
}
case 'blink': {
const fx0 = num(ex.fx ?? ex.fromX, NaN), fy0 = num(ex.fy ?? ex.fromY, NaN);
if (Number.isFinite(fx0) && Number.isFinite(fy0)) {
const q = cam.project(fx0, fy0, at.z + 0.5, this._q);
this.smoke(q.x, q.y, q.s * 0.6, 0x2c2436, 0.45);
this.particle('glow', q.x, q.y, { tint: col, life: 0.3, s0: q.s / 128 * 0.8, s1: q.s / 128 * 0.2, a0: 0.8, a1: 0 });
this.streak(fx0, fy0, at.x, at.y, at.z + 0.5, col, 0.3);
}
this.particle('glow', p.x, p.y, { tint: col, life: 0.35, s0: s / 128 * 0.3, s1: s / 128 * 1.2, a0: 0.9, a1: 0 });
this.burst(p.x, p.y, s, 6, col, { speed: 1.4, tex: 'dot', life: 0.4 });
break;
}
case 'move': {
const fx0 = num(ex.fromX ?? ex.fx ?? ex.x0, NaN), fy0 = num(ex.fromY ?? ex.fy ?? ex.y0, NaN);
const tx = num(ex.tx, NaN), ty = num(ex.ty, NaN);
if (Number.isFinite(fx0) && Number.isFinite(fy0)) this.streak(fx0, fy0, at.x, at.y, at.z + 0.35, col, 0.35);
else if (Number.isFinite(tx) && Number.isFinite(ty)) this.streak(at.x, at.y, tx, ty, at.z + 0.35, col, 0.35);
const g = cam.project(at.x, at.y, at.z + 0.05);
for (let i = 0; i < (this.quality === 'low' ? 2 : 5); i++) {
this.particle('smoke', g.x + (Math.random() - 0.5) * g.s * 0.5, g.y, { add: false, tint: 0x6b6358, vx: (Math.random() - 0.5) * g.s * 0.6, vy: -g.s * 0.2, drag: 2, life: 0.5, s0: g.s / 128 * 0.25, s1: g.s / 128 * 0.55, a0: 0.45, a1: 0 });
}
break;
}
// a travelling blade (赤刃明霄陈 S3's 龙剑气): a short trail from where it was to where it is, plus a crescent head
// turned the way it moves — the sim sends one of these every 0.12 s, and the trail outlives that gap so the
// events join into one sweeping wave. A procedural stand-in (the official effect is a particle prefab this client
// does not extract). Trail, head and heading share one height; the head is projected after streak(), which
// reuses this._p / this._q (projected before it, the head was drawn at the trail's start, one event behind)
case 'qi': {
const z = at.z + 0.35;
const fx0 = num(ex.fromX, NaN), fy0 = num(ex.fromY, NaN);
if (Number.isFinite(fx0) && Number.isFinite(fy0)) this.streak(fx0, fy0, at.x, at.y, z, col, 0.34);
const h = cam.project(at.x, at.y, z, this._p), hx = h.x, hy = h.y, hs = h.s;
const dr = num(ex.dr, NaN), dc = num(ex.dc, NaN);
let rot = 0;
if (Number.isFinite(dr) && Number.isFinite(dc) && (dr || dc)) {
const q = cam.project(at.x + dc * 0.5, at.y + dr * 0.5, z, this._q);
// the `slash` sprite's crescent bulges along its local −y, so this turns that edge onto the travel direction
rot = Math.atan2(q.x - hx, -(q.y - hy));
}
this.particle('slash', hx, hy, { tint: col, life: 0.3, s0: hs / 128 * 1.3, s1: hs / 128 * 2.0, a0: 0.9, a1: 0, rot });
this.particle('glow', hx, hy, { tint: col, life: 0.26, s0: hs / 128 * 0.8, s1: hs / 128 * 1.5, a0: 0.55, a1: 0 });
break;
}
case 'wave': {
const k = clamp(num(ex.scale, 1), 0.5, 4);
const rr = Math.max(0.8, r * (ex.scale ? Math.min(2, k / 2) : 1));
for (let i = 0; i < 3; i++) this.ring(at.x, at.y, at.z, 0.15 + i * 0.15, rr * (0.7 + i * 0.25), col, 0.45 + i * 0.12);
this.particle('glow', p.x, p.y, { tint: col, life: 0.3, s0: s / 128 * 0.5, s1: s / 128 * 1.2, a0: 0.7, a1: 0 });
break;
}
case 'mark': case 'reticle': {
if (kind === 'lock') {
// `id` is always the locked enemy: the reticle sticks to its view even a little off the event's spot
// (`hold`: 蕾缪安 S3 — the reticle waits while her skill runs, however long nothing is in range)
const lv = at.v || this._viewOf(ex.id);
this._touchLocks(ex.src ?? null);
this._lock(lv, ex.src ?? null, lv ? lv.x : at.x, lv ? lv.y : at.y, lv ? bodyZ(cam, lv, SHOT_HEIGHT.aim) : at.z + 0.55, !!ex.hold);
break;
}
const v = at.v;
const hz = v ? (v.z || 0) + (v.hover || 0) + (v._headTiles || 1.2) + 0.25 : at.z + 1.4;
const q = cam.project(at.x, at.y, hz, this._q);
if (spec.a === 'mark') {
this.particle('glow', q.x, q.y, { tint: col, life: 0.6, s0: q.s / 128 * 0.5, s1: q.s / 128 * 0.7, a0: 0.8, a1: 0 });
this.numberAt(q.x, q.y, '!', col, 0.8);
} else {
this.particle('ring', p.x, p.y, { tint: col, life: 0.6, s0: s / 128 * 1.3, s1: s / 128 * 0.7, a0: 0.95, a1: 0, spin: 3 });
this.particle('hex', p.x, p.y, { tint: col, life: 0.6, s0: s / 128 * 0.5, s1: s / 128 * 0.9, a0: 0.8, a1: 0, spin: -2 });
}
break;
}
case 'buff': {
const g = cam.project(at.x, at.y, at.z + 0.05);
const n = this.quality === 'low' ? 3 : 6;
for (let i = 0; i < n; i++) {
const ox = (Math.random() - 0.5) * g.s * 0.7;
this.particle('chevron', g.x + ox, g.y - g.s * (0.1 + Math.random() * 0.5), { tint: col, vy: -g.s * (1 + Math.random() * 0.5), life: 0.65, s0: g.s / 64 * 0.22, s1: g.s / 64 * 0.12, a0: 0.95, a1: 0, rot: -Math.PI / 2, fadeIn: 0.06 });
}
this.ring(at.x, at.y, at.z, 0.2, 0.75, col, 0.4);
this.particle('glow', p.x, p.y, { tint: col, life: 0.35, s0: s / 128 * 0.6, s1: s / 128 * 1.1, a0: 0.6, a1: 0 });
break;
}
case 'lift': {
const g = cam.project(at.x, at.y, at.z);
this.ring(at.x, at.y, at.z, 0.2, 0.8, col, 0.5);
for (let i = 0; i < (this.quality === 'low' ? 3 : 7); i++) {
this.particle('streak', g.x + (Math.random() - 0.5) * g.s * 0.6, g.y - g.s * Math.random() * 0.4, { tint: col, vy: -g.s * 1.8, life: 0.4, s0: g.s / 128 * 0.4, s1: g.s / 128 * 0.2, a0: 0.8, a1: 0, rot: -Math.PI / 2, sx: 1 });
}
break;
}
case 'sleep': {
const v = at.v;
const hz = v ? (v.z || 0) + (v._headTiles || 1.2) : at.z + 1.2;
const q = cam.project(at.x + 0.2, at.y, hz, this._q);
for (let i = 0; i < 3; i++) this.particle('st_sleep', q.x + i * q.s * 0.12, q.y - i * q.s * 0.12, { add: false, vy: -q.s * 0.5, vx: q.s * 0.15, life: 0.9 + i * 0.2, s0: q.s / 32 * 0.22, s1: q.s / 32 * 0.32, a0: 1, a1: 0, fadeIn: 0.1 * i });
break;
}
case 'crit': {
this.particle('spark', p.x, p.y, { tint: col, life: 0.28, s0: s / 64 * 1.3, s1: s / 64 * 0.2, a0: 1, a1: 0, rot: Math.random() * Math.PI });
this.particle('glow', p.x, p.y, { tint: 0xffffff, life: 0.18, s0: s / 128 * 0.9, s1: s / 128 * 1.4, a0: 0.9, a1: 0 });
this.burst(p.x, p.y, s, 8, col, { speed: 3.2, life: 0.3, size: 0.5 });
break;
}
case 'dodge': {
this.particle('soft', p.x, p.y, { tint: 0xffffff, life: 0.3, s0: s / 128 * 0.5, s1: s / 128 * 0.9, a0: 0.5, a1: 0 });
this.particle('slash', p.x, p.y, { tint: col, life: 0.22, s0: s / 128 * 0.8, s1: s / 128 * 1.1, a0: 0.7, a1: 0, rot: -0.5 });
break;
}
case 'counter': {
this.particle('spark', p.x, p.y, { tint: col, life: 0.22, s0: s / 64 * 0.9, s1: 0, a0: 1, a1: 0 });
this.particle('slash', p.x, p.y, { tint: col, life: 0.22, s0: s / 128 * 0.9, s1: s / 128 * 1.2, a0: 0.9, a1: 0, rot: 0.6 });
this.burst(p.x, p.y, s, 4, col, { speed: 2.4, life: 0.25 });
break;
}
case 'dp': {
const n = Math.round(num(ex.n, 0));
this.particle('shard', p.x, p.y, { tint: col, vy: -s * 0.6, life: 0.9, s0: s / 32 * 0.28, s1: s / 32 * 0.22, a0: 1, a1: 0 });
if (n > 0) this.numberAt(p.x + s * 0.2, p.y, `+${n}`, col, 0.7);
break;
}
case 'coin': {
for (let i = 0; i < 4; i++) this.particle('coin', p.x + (Math.random() - 0.5) * s * 0.4, p.y, { add: false, vx: (Math.random() - 0.5) * s, vy: -s * (1.2 + Math.random() * 0.6), g: s * 4, life: 0.7, s0: s / 48 * 0.2, s1: s / 48 * 0.16, a0: 1, a1: 0.2 });
break;
}
case 'crate': this.crateBreak(at.x, at.y, at.z); break;
case 'down': {
this.smoke(p.x, p.y, s * 0.6, spec.smoke ?? 0x1a1a1a, 0.5);
this.burst(p.x, p.y, s, 5, col, { speed: 1.2, tex: 'dot', life: 0.5, g: 2 });
break;
}
case 'beam': case 'bolt': {
const a = this._viewOf(ex.from ?? ex.src) || (spec.a === 'bolt' ? null : at.v);
const b = this._viewOf(ex.to ?? ex.target) || (a === at.v ? null : at.v);
if (a && b && a !== b) this._beam(a, b, col, spec.a === 'bolt' ? 0.28 : 0.4, spec.a === 'bolt' ? 1 : 0.4);
else this.strike(at.x, at.y, at.z, col);
break;
}
case 'flame': {
// at the event's spot (the locked target), not snapped onto the dragon hovering 0.25 tile from it
const fx0 = Number(x), fy0 = Number(y);
this._flame(ex.id ?? ex.src ?? null, ex.target ?? ex.to ?? null, fx0, fy0, r, col, Math.max(0.2, dur || 0.5));
break;
}
case 'strike': case 'pillar': this.strike(at.x, at.y, at.z, col, spec.a === 'pillar'); break;
case 'volley': {
const list = Array.isArray(ex.targets) ? ex.targets.slice(0, 12) : [];
const src = this._viewOf(ex.src ?? ex.id);
for (const id of list) {
const t = this._viewOf(id);
if (!t) continue;
if (src && src !== t) this.attack(src, t, 'arrow');
else { const q = this._chest(t, this._q); this.burst(q.x, q.y, q.s, 4, col, { speed: 2, life: 0.3 }); }
}
if (!list.length) this.burst(p.x, p.y, s, 6, col, { speed: 2.4, life: 0.35 });
break;
}
case 'lp': this.leak(); break;
case 'element': {
const el = ex.element;
const c2 = el === 'burn' ? 0xff7a33 : el === 'neural' ? 0xff5ad0 : el === 'necrosis' || el === 'apoptosis' ? 0x9dff6a : el === 'erosion' ? 0x6fe0ff : col;
this.particle('glow', p.x, p.y, { tint: c2, life: 0.45, s0: s / 128 * 1.2, s1: s / 128 * 2.6, a0: 1, a1: 0 });
this.ring(at.x, at.y, at.z, 0.2, 1.5, c2, 0.55);
this.burst(p.x, p.y, s, 12, c2, { speed: 3, life: 0.5 });
break;
}
default: {
this.particle('spark', p.x, p.y, { tint: col, life: 0.35, s0: s / 64 * 0.6, s1: 0, a0: 0.9, a1: 0, rot: Math.random() });
this.particle('glow', p.x, p.y, { tint: col, life: 0.3, s0: s / 128 * 0.4, s1: s / 128 * 0.9, a0: 0.6, a1: 0 });
this.burst(p.x, p.y, s, 4, col, { speed: 1.6, tex: 'dot', life: 0.35 });
}
}
}
}