- #1 picking by the tile under the pointer (prep, battle, pen, equipment drops); the dragged model stays under the pointer (mouse and touch); pixel probe removed; phone ghost-click on the underframe fixed - #2 two-tap confirm on 机变 cards (like the shop) - #3 a 同盟 room with one human is untimed like 独立模拟 (no countdown outside battles) - #4 strategy draft: one synced countdown, 30 s per human turn, timeout takes the highlighted band - #5 special 维式重锤 (4) and 突变细胞 no longer sold; item card shows where they come from - #6 operator skill-mode sounds no longer play on normal attacks (纯烬艾雅法拉 and 55 others) - #7 detail card shows live battle stats and the prep-time battle-start stats (g.unitStats) - #8 official element gauges on units; burst cooldown (阻回) per PRTS - #9 knocked-down operators stay on their tile with a redeploy countdown ring - #10 drone behaviour per PRTS (暴鸰, 先兆者 shells, 黑云, 萨科塔之眼) - #11 赫默's drone only from her skill; placeable summons limited to talent tokens - #12 炎佑 stays where it is with no enemies; 祛恶之焰 as a continuous channel - #13 the occasional giant bond icon (loading texture scaled ×46) fixed - docs (DESIGN §18), recordings, data and tests updated (2467 unit tests, all browser suites green)
110 lines
5.2 KiB
JavaScript
110 lines
5.2 KiB
JavaScript
// test/render/fxflame.test.js — 炎佑's 祛恶之焰 as a continuous jet of fire (render/fx.js _flame; user playtest #4 item 12
|
||
// "官方是一段持续时间的喷火"). The sim (server/sim/content/tokens.js yanyouKit) emits one fx 'yanyouFlame'
|
||
// {x, y, id: dragon, target, r, n, dur: 1} per game second of the channel; the renderer keeps ONE jet per dragon that each
|
||
// tick re-aims and extends, so the ticks join into a stream from the dragon onto its locked target with a burning disc
|
||
// of radius r around the target. It used to be the generic 'buff' sparkle once per second.
|
||
// Against the headless fake PIXI (test/render/fakepixi.js), like fxproj.test.js.
|
||
|
||
import { test, describe, before, after } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { installFakePixi, fakeViewCtx } from './fakepixi.js';
|
||
import { presetCamera } from '../../public/js/render/projection.js';
|
||
|
||
let fake, FX;
|
||
before(async () => {
|
||
fake = installFakePixi();
|
||
FX = await import('../../public/js/render/fx.js');
|
||
});
|
||
after(() => fake.restore());
|
||
|
||
const DT = 1 / 120;
|
||
const cam = presetCamera('normal', { width: 1600, height: 900 });
|
||
const unit = (id, x, y, o = {}) => ({ id, x, y, z: 0, hover: 0, _headTiles: 1.2, alive: true, destroyed: false, info: { defId: 'x' }, onHit() {}, ...o });
|
||
|
||
function makeFx(views, ts = 2) {
|
||
const P = fake.P;
|
||
const ctx = fakeViewCtx(P);
|
||
const map = new Map(views.map((v) => [v.id, v]));
|
||
const fx = new FX.FxSystem({
|
||
P, layers: ctx.layers, cam: () => cam, heightAt: () => 0, settings: { quality: 'high', damageNumbers: true },
|
||
timeScale: () => ts, loadLevel: () => 0, subProfOf: () => null, view: (id) => map.get(id) || null,
|
||
screenSize: () => ({ width: 1600, height: 900 }), fieldTop: () => 120,
|
||
});
|
||
return { fx, map };
|
||
}
|
||
const run = (fx, seconds) => { for (let t = 0; t < seconds - 1e-9; t += DT) fx.update(DT); };
|
||
/** One sim tick of the channel (the event tokens.js emits). */
|
||
const tick = (fx, dragon, target, r = 1) => fx.simFx('yanyouFlame', target.x, target.y, { x: target.x, y: target.y, id: dragon.id, target: target.id, r, n: 1, dur: 1 });
|
||
|
||
describe('炎佑 祛恶之焰: one continuous jet of fire (user playtest #4 item 12)', () => {
|
||
test('its own archetype (no longer the one-off buff sparkle)', () => {
|
||
assert.equal(FX.FX_KINDS.yanyouFlame.a, 'flame');
|
||
assert.equal(FX.fxSpec('yanyouFlame').a, 'flame');
|
||
});
|
||
|
||
test('the 1 s ticks join into one stream that burns while they come and ends after the last one', () => {
|
||
const dragon = unit(40, 6.2, 10.1, { hover: 0.32 }), foe = unit(20, 6, 10);
|
||
const { fx } = makeFx([dragon, foe]);
|
||
for (let s = 0; s < 6; s++) { // 6 game s of channel at the 2× clock: a tick every 0.5 real s
|
||
tick(fx, dragon, foe);
|
||
assert.equal(fx.flames.length, 1, `tick ${s}: still one jet`);
|
||
run(fx, 0.5);
|
||
assert.equal(fx.flames.length, 1, `between ticks ${s} and ${s + 1} the jet keeps burning`);
|
||
const F = fx.flames[0];
|
||
assert.ok(F.disc.alpha > 0.2 && F.edge.alpha > 0.2, 'the burning disc shows');
|
||
}
|
||
assert.ok(fx.counts.particles > 20, 'fire streams from the dragon and rises from the disc');
|
||
run(fx, 0.3);
|
||
assert.equal(fx.flames.length, 0, 'no tick came: the jet goes out within FLAME_TAIL');
|
||
});
|
||
|
||
test('the disc follows the locked target; a new tick re-aims the same jet', () => {
|
||
const dragon = unit(40, 3, 10, { hover: 0.32 }), a = unit(20, 3.2, 10), b = unit(21, 5, 11);
|
||
const { fx } = makeFx([dragon, a, b]);
|
||
tick(fx, dragon, a);
|
||
run(fx, 0.1);
|
||
const F = fx.flames[0];
|
||
const at0 = { x: F.disc.position.x, y: F.disc.position.y };
|
||
a.x = 4.2; // the target walks on: the disc goes with it
|
||
run(fx, 0.1);
|
||
assert.ok(F.disc.position.x > at0.x + 20, `disc follows the target (${at0.x} → ${F.disc.position.x})`);
|
||
tick(fx, dragon, b); // the next tick aims at another enemy: the same jet, re-aimed
|
||
run(fx, 0.1);
|
||
assert.equal(fx.flames.length, 1);
|
||
assert.equal(fx.flames[0], F);
|
||
assert.equal(F.tgt, 21);
|
||
assert.equal(F.r, 1);
|
||
});
|
||
|
||
test('the dragon gone (knocked out, left the field) puts its jet out at once; clear() drops every jet', () => {
|
||
const dragon = unit(40, 3, 10), foe = unit(20, 3.2, 10);
|
||
const { fx, map } = makeFx([dragon, foe]);
|
||
tick(fx, dragon, foe);
|
||
run(fx, 0.05);
|
||
assert.equal(fx.flames.length, 1);
|
||
map.delete(40);
|
||
run(fx, 0.05);
|
||
assert.equal(fx.flames.length, 0, 'no dragon, no fire');
|
||
map.set(40, dragon);
|
||
tick(fx, dragon, foe);
|
||
const disc = fx.flames[0].disc;
|
||
fx.clear();
|
||
assert.equal(fx.flames.length, 0);
|
||
assert.ok(disc.destroyed, 'its sprites are freed');
|
||
});
|
||
|
||
test('two dragons (9 炎) burn two jets; a target that died keeps the jet on its last spot until it fades', () => {
|
||
const d1 = unit(40, 3, 10), d2 = unit(41, 3, 10.8), foe = unit(20, 3.2, 10);
|
||
const { fx, map } = makeFx([d1, d2, foe]);
|
||
tick(fx, d1, foe); tick(fx, d2, foe);
|
||
assert.equal(fx.flames.length, 2);
|
||
run(fx, 0.1);
|
||
const x = fx.flames[0].x;
|
||
map.delete(20); // killed
|
||
run(fx, 0.1);
|
||
assert.equal(fx.flames[0].x, x, 'stays where the target was');
|
||
run(fx, 0.6);
|
||
assert.equal(fx.flames.length, 0);
|
||
});
|
||
});
|