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

+50
View File
@@ -404,3 +404,53 @@ test('live leaks: a boss field is not counted (the merged team LP moves through
assert.deepEqual(r2.store.get().match.battle.leaks, {}, 'only normal fields');
r2.runner.dispose();
});
test('live unit stats (user playtest #4 item 7): unitStats(id) reads the battle on screen — the sim\'s last computed stats, never unit.s (no recompute from the UI)', async () => {
const start = realStart(7305);
const r = rig();
r.net.emit('b.start', start);
await r.settle();
const e = r.runner._entries.get(start.battleId);
r.advance(2000);
const ally = e.battle.allyUnits.find((u) => u.alive && u.deployed && u.kind === 'op');
assert.ok(ally, 'a deployed operator');
// a trap on the lazy getter: reading the live stats must not recompute them (it would move the sim's floats)
const s = ally._s;
Object.defineProperty(ally, 's', { configurable: true, get() { throw new Error('unit.s read by the UI'); } });
const got = r.runner.unitStats(ally.id);
delete ally.s;
assert.ok(got, 'the unit of the battle on screen');
assert.equal(got.id, ally.id);
assert.equal(got.uid, ally.uid);
assert.equal(got.defId, ally.defId);
assert.equal(got.atk, Math.round(s.atk));
assert.equal(got.maxHp, Math.round(s.maxHp));
assert.equal(got.def, Math.round(s.def));
assert.equal(got.interval, Math.round(s.interval * 100) / 100);
assert.equal(got.blockCnt, s.blockCnt);
assert.equal(got.hp, Math.round(ally.hp));
assert.equal(got.base.atk, Math.round(ally.base.atk));
assert.equal(got.base.maxHp, Math.round(ally.base.maxHp));
assert.equal(r.runner.unitStats(ally.id, start.fieldId)?.id, ally.id, 'on the named field');
assert.equal(r.runner.unitStats(ally.id, 'n:someone_else'), null, 'another field: nothing');
assert.equal(r.runner.unitStats(999999), null, 'unknown unit');
// an own board piece's card left open into the battle: its unit by uid and owner
assert.equal(r.runner.unitIdOf(ally.uid, ally.ownerId), ally.id);
assert.equal(r.runner.unitIdOf(ally.uid, ally.ownerId, start.fieldId), ally.id);
assert.equal(r.runner.unitIdOf(ally.uid, 'someone_else'), null, 'another player\'s piece');
assert.equal(r.runner.unitIdOf(ally.uid, ally.ownerId, 'n:someone_else'), null, 'another field');
assert.equal(r.runner.unitIdOf(null, ally.ownerId), null);
assert.equal(r.runner.unitStats('x'), null);
// enemies too, once one is out
for (let i = 0; i < 60 && !e.battle.enemies.some((x) => x.alive); i++) r.advance(500, 50);
const foe = e.battle.enemies.find((x) => x.alive);
if (foe) {
const fs = r.runner.unitStats(foe.id);
assert.ok(fs && fs.maxHp > 0 && fs.base.maxHp > 0, 'an enemy\'s live stats');
assert.equal(fs.moveSpeed, Math.round((foe._s || foe.base).moveSpeed * 100) / 100);
}
r.runner.clear();
assert.equal(r.runner.unitStats(ally.id), null, 'nothing on screen after the battles were dropped');
assert.equal(r.runner.unitIdOf(ally.uid, ally.ownerId), null);
r.runner.dispose();
});