Playtest #4 fixes: tile picking, knocked-down state, element gauges, draft timer, live stats, shop items, drones, 赫默, 炎佑, UI flash

- #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)
This commit is contained in:
sganggs committed 2026-09-30 05:35:45 +08:00
1 parent 0aba602ecf
commit d4f868cd19
107 files changed
+4510 -1653

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
// 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);
});
});