Files
ark-stronghold-protocol/test/render/fxpop.test.js
T
sganggs d4f868cd19 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)
2026-09-30 05:35:45 +08:00

93 lines
4.8 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.
// test/render/fxpop.test.js — user playtest #4 item 13 ("屏幕很偶尔的情况下会有个巨大的ui的一部分遮挡一两秒": once in a while a
// huge piece of UI covered the screen for a second or two). The first in-battle bond layer gain of each bond (b.ev
// 'layer' → render/app.js → FxSystem.pop) popped that bond's icon 46 times too big — ~5000 px, the whole screen, for
// the pop's 1.4 s. The icon is PIXI.Texture.from(url): a 1×1 placeholder until its image has loaded (then the real size
// and an 'update' event, Pixi 7.4), and pop() sized the sprite from the placeholder (46 / 1). Reproduced in headless
// Chrome (test/render/flash.browser.test.js); here against a fake PIXI with the same Texture semantics.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { FxSystem } from '../../public/js/render/fx.js';
class Emitter {
constructor() { this._on = new Map(); }
on(e, f, ctx) { if (!this._on.has(e)) this._on.set(e, []); this._on.get(e).push({ f, ctx, once: false }); return this; }
once(e, f, ctx) { if (!this._on.has(e)) this._on.set(e, []); this._on.get(e).push({ f, ctx, once: true }); return this; }
off(e, f) { const l = this._on.get(e); if (l) this._on.set(e, l.filter((x) => x.f !== f)); return this; }
emit(e, ...a) { const l = this._on.get(e) || []; this._on.set(e, l.filter((x) => !x.once)); for (const x of l) x.f.apply(x.ctx, a); return true; }
}
/** Pixi 7.4 Texture of an image: 1×1 and invalid until its base texture has loaded, then its size + 'update'. */
class Tex extends Emitter {
constructor(w, h, loaded = true) {
super();
this._size = [w, h];
this.baseTexture = { valid: loaded, width: loaded ? w : 1, height: loaded ? h : 1 };
this.orig = { width: loaded ? w : 1, height: loaded ? h : 1 };
this.valid = loaded;
}
get width() { return this.orig.width; }
get height() { return this.orig.height; }
load() {
[this.orig.width, this.orig.height] = this._size;
Object.assign(this.baseTexture, { valid: true, width: this._size[0], height: this._size[1] });
this.valid = true;
this.emit('update', this);
}
}
class Point { constructor(x = 1, y = x) { this.x = x; this.y = y; } set(x, y = x) { this.x = x; this.y = y; } }
class Sprite {
constructor(tex) { this.texture = tex; this.visible = true; this.scale = new Point(1); this.anchor = new Point(0); this.position = new Point(0); this.tint = 0xffffff; this.destroyed = false; }
destroy() { this.destroyed = true; }
}
class Container {
constructor() { this.children = []; this.position = new Point(0); this.scale = new Point(1); this.alpha = 1; this.destroyed = false; }
addChild(...c) { this.children.push(...c); return c[0]; }
destroy() { this.destroyed = true; for (const c of this.children) c.destroy?.(); }
}
class BitmapText { constructor(text) { this.text = text; this.anchor = new Point(0); this.position = new Point(0); this.tint = 0xffffff; } destroy() {} }
function fx() {
const f = Object.create(FxSystem.prototype);
Object.assign(f, {
P: { Sprite, Container, BitmapText, BLEND_MODES: { ADD: 1 } },
tex: { glow: new Tex(64, 64) }, pops: [],
ctx: { screenSize: () => ({ width: 1920, height: 1080 }), fieldTop: () => 200, layers: { screen: { addChild() {} } } },
});
return f;
}
/** Screen px of the icon sprite's longer side as drawn (0 while hidden). */
const drawn = (sp) => (sp.visible ? Math.max(sp.texture.width * Math.abs(sp.scale.x), sp.texture.height * Math.abs(sp.scale.y)) : 0);
const iconOf = (f, icon) => f.pops.at(-1).c.children.find((c) => c.texture === icon);
describe('FxSystem.pop: the bond icon is 46 px whenever its image loads (user playtest #4 item 13)', () => {
test('an icon still loading is sized when it has loaded — never from its 1×1 placeholder', () => {
const f = fx();
const icon = new Tex(110, 98, false); // bond/yanShip.png
f.pop(icon, '+2', 0xffffff, 0);
const sp = iconOf(f, icon);
assert.ok(drawn(sp) <= 46.5, `nothing big while loading (${drawn(sp)} px)`);
icon.load();
assert.ok(Math.abs(drawn(sp) - 46) < 0.5, `46 px once loaded (was 110 × 46 = 5060 px): ${drawn(sp)}`);
f._updatePops(0.5);
assert.ok(drawn(sp) * f.pops[0].c.scale.x < 60, 'with the pop\'s own scale-in');
});
test('an icon already loaded: 46 px along its longer side at once', () => {
const f = fx();
const icon = new Tex(102, 100);
f.pop(icon, '+1', 0xffffff, 1);
assert.ok(Math.abs(drawn(iconOf(f, icon)) - 46) < 0.5);
});
test('a pop that ended before its image loaded stays gone', () => {
const f = fx();
const icon = new Tex(110, 98, false);
f.pop(icon, '+3', 0xffffff, 0);
const sp = iconOf(f, icon);
f._updatePops(2);
assert.equal(f.pops.length, 0);
assert.doesNotThrow(() => icon.load());
assert.ok(sp.destroyed);
});
});