Files
ark-stronghold-protocol/test/ui/inspect-range.e2e.test.js
T
IceCodeNew 0ca2a9c21a feedback7 R22-LATE: an open detail card draws its field unit's current attack range — own board pieces in prep, any ally operator / summon in battle or on a scouted board; golden: unchanged
GitHub PR #281 by @IceCodeNew (this commit is substantially the PR, ported onto 0.2.2-dev; the field-detail range
idea after @BBleae's fork commits c6437f1 / b451bad, credited here instead of the PR's Co-authored-by trailers). The
unofficial skill-range preview the PR first had is gone (the owner's reply of 2026-10-07: only official-like
behaviour); what remains is the range the unit fights with now.

screens/game/range.js inspectRange resolves the open card's unit: an own board piece in prep (its loadout or
stand-in record, its facing — this replaces the prep selection's range effect); in battle or on a scouted board an
ally op / token on the field, at the snapshot's tile, with the live stats' range when there is one; never an enemy,
a dead or undeployed unit, or a scouted bench unit (UnitInfo `area` hand / temp — the tag the server sends since the
#385 fix; renderInfo now keeps it). shared/protocol.js unitStatsEntry gains an optional `dir` (the unit's facing now:
the snapshot tuples carry none and UnitInfo.dir is the facing at send time). tokenVariantFor moves to
gameLogic/loadout.js. Drag, the wheel and the pen clear the overlay.

Added on top of the PR: a test from the server's own scout m.field (board operator ranged, hand / temp none), docs
(PLAYING.md, design/client.md, design/network.md), comments.
Tests: test/ui/inspect-range.test.js (6), test/ui/playtest6_summons.test.js, test/match/unitStats.test.js;
test/ui/inspect-range.e2e.test.js (SP_E2E + CHROME_PATH: run here, 1/1). Golden unchanged (display; m.unitStats only).
2026-10-08 21:27:33 +08:00

93 lines
5.3 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import { startServer } from '../../server/index.js';
import { makeBattle } from '../helpers/battleHarness.js';
import { spawnMapChar } from '../../server/sim/content/tokens.js';
const chrome = process.env.CHROME_PATH;
const enabled = process.env.SP_E2E === '1' && chrome && existsSync(chrome);
test('user Given a real battle with strategy medics When clicking units and closing details Then attack tiles follow selection',
{ skip: !enabled && 'Set SP_E2E=1 and CHROME_PATH', timeout: 60000 }, async () => {
const server = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
const puppeteer = (await import('puppeteer-core')).default;
const browser = await puppeteer.launch({ executablePath: chrome, headless: true, args: ['--no-sandbox'] });
try {
for (const medicId of ['char_613_acmedc', 'char_605_cmedic']) {
const h = makeBattle({ stageId: 'act2autochess_m01', units: [{ chessId: 'chess_char_1_01_a', row: 10, col: 4 }], autoFinish: false });
h.step();
assert.ok(spawnMapChar(h.b, h.b.players[0].playerId, medicId));
h.step();
const field = h.b.fieldMeta();
const snapshot = h.b.snapshot();
assert.equal(field.units.length, 2);
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 2 });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(`http://127.0.0.1:${server.port}/dev/game-mock.html?render=fallback&shot=1&phase=PREP`);
await page.waitForSelector('.ff-piece');
await page.evaluate(async ({ field, snapshot }) => {
const { store } = await import('/js/store.js');
const { net } = await import('/js/net.js');
const pub = store.get().match.public;
store.patch('match', { public: { ...pub, phase: 'COMBAT', fields: [{ fieldId: field.fieldId, players: [pub.players[0].playerId] }] }, field });
// The local runner emits the snapshot synchronously, before the screen's field-entry effect.
net._emit('b.snap', snapshot);
}, { field, snapshot });
await page.waitForSelector('.ff-unit');
for (let i = 0; i < field.units.length; i++) {
const units = await page.$$('.ff-unit');
await units[i].click();
await page.waitForSelector('.dpanel');
await page.waitForSelector('.ff-tile.is-range');
const lit = await page.$$eval('.ff-tile.is-range', (els) => els.map((e) => [Number(e.dataset.row), Number(e.dataset.col)]));
const u = field.units[i];
if (u.defId === medicId) {
assert.ok(lit.some(([r, c]) => r === 10 && c === 5), 'medic reaches three columns right from the stage tile');
assert.ok(!lit.some(([, c]) => c === 6), 'inactive skill does not extend the base range');
}
// A new wire snapshot must refresh the open overlay, not just the unit sprite.
await page.evaluate(async ({ snapshot, id }) => {
const { net } = await import('/js/net.js');
net._emit('b.snap', { ...snapshot, units: snapshot.units.map((v) => v[0] === id ? [v[0], v[1] + 1, ...v.slice(2)] : v) });
}, { snapshot, id: u.id });
await page.waitForFunction((before) => {
const now = [...document.querySelectorAll('.ff-tile.is-range')].map((e) => [Number(e.dataset.row), Number(e.dataset.col)]);
return now.length === before.length && now.every(([r, c]) => before.some(([br, bc]) => br === r && bc + 1 === c));
}, {}, lit);
await page.evaluate(async () => {
const { store } = await import('/js/store.js');
store.patch('match', { public: { ...store.get().match.public, phase: 'SETTLE' } });
// Allow Preact's after-paint phase effects to run before asserting retained UI.
await new Promise((resolve) => setTimeout(resolve, 250));
});
assert.ok(await page.$('.dpanel'), 'same-field settlement keeps the inspected card');
assert.ok(await page.$('.ff-tile.is-range'), 'same-field settlement keeps its range');
await page.click('.dpanel__close');
await page.waitForFunction(() => !document.querySelector('.ff-tile.is-range'));
await page.evaluate(async (snapshot) => {
const { net } = await import('/js/net.js');
net._emit('b.snap', snapshot);
}, snapshot);
}
await (await page.$$('.ff-unit'))[0].click();
await page.waitForSelector('.ff-tile.is-range');
await page.evaluate(async ({ field, snapshot }) => {
const { store } = await import('/js/store.js');
const { net } = await import('/js/net.js');
const fieldId = `${field.fieldId}:next`;
store.patch('match', { field: { ...field, fieldId } });
net._emit('b.snap', { ...snapshot, fieldId });
}, { field, snapshot });
await page.waitForFunction(() => !document.querySelector('.dpanel') && !document.querySelector('.ff-tile.is-range'));
assert.deepEqual(errors, []);
await page.close();
}
} finally {
await browser.close();
await server.close();
}
});