- #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)
93 lines
4.8 KiB
JavaScript
93 lines
4.8 KiB
JavaScript
// 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);
|
||
});
|
||
});
|