Files
ark-stronghold-protocol/test/render/feedback1f-down-tile.browser.test.js
sganggs 8de9002d1a feedback1f WV: the client draws a knocked-out operator on the tile it lies on
render/interp.js keeps the body tile of a b.snap `down` entry (row, col; only
when both are integers, so a 4-element entry is unchanged) and
render/units.js setDown moves the knocked-down view and its ring there: an
operator that fell on another piece's home lies on its own home (PRTS
卫戍协议/帮助 "…尝试返回其自身的初始位置"), and a view built from the unit's
deploy info (a field joined mid-battle) no longer stays on its first tile.

Tests: test/render/feedback1f-down-tile.test.js (fake PIXI, the sim's own
snapshot); opt-in test/render/feedback1f-down-tile.browser.test.js (real sim
frames in headless Chrome: drawn, picked and redeployed on its own home).
2026-10-03 07:16:34 +08:00

114 lines
6.4 KiB
JavaScript
Raw Permalink 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.
// Player report F5 after the 0.1.0 release in headless Chrome through the render demo (public/dev/render-demo.html,
// real Spine models): the real sim's frames (Battle.snapshot / drainEvents, fed like the client runner's local feed) of
// an operator that jumped onto a teammate's home tile (a 突袭 landing) and was knocked out there. Officially it goes back
// to its own home ("若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置", PRTS
// 卫戍协议/帮助): the knocked-down model and its ring are drawn on its own home (b.snap `down` row / col), a press there
// selects it, the teammate's home stays empty, and it redeploys on that tile.
//
// Opt-in (starts Chrome): RENDER_E2E=1 node --test test/render/feedback1f-down-tile.browser.test.js
// Chrome path: $CHROME_PATH or the macOS default. Screenshots → test/e2e/out/feedback1f-down-*.png.
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { makeBattle } from '../helpers/battleHarness.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const OUT = path.join(ROOT, 'test/e2e/out');
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const enabled = process.env.RENDER_E2E === '1' && existsSync(CHROME) && existsSync(path.join(ROOT, 'public/assets'));
const skip = enabled ? false : 'set RENDER_E2E=1 (needs Chrome and downloaded assets)';
describe('a knocked-out operator is drawn on the tile it lies on (real sim frames, headless Chrome)', { skip }, () => {
let srv, browser;
before(async () => {
const puppeteer = (await import('puppeteer-core')).default;
const { startServer } = await import('../../server/index.js');
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-first-run'] });
mkdirSync(OUT, { recursive: true });
});
after(async () => {
await browser?.close();
await srv?.close();
});
test('fell on a teammate\'s home → drawn, picked and redeployed on its own home', async () => {
// 宴 (home 12,3) and 休谟斯 (home 10,3), real chess on 战场#01; no enemies, the battle keeps running
const h = makeBattle({ stageId: 'act2autochess_m01', units: [{ chessId: 'chess_char_1_18_a', row: 12, col: 3 }, { chessId: 'chess_char_2_09_a', row: 10, col: 3 }], autoFinish: false, timeLimit: 300 });
const b = h.b;
b.step();
const A = h.unit('chess_char_1_18_a'), B = h.unit('chess_char_2_09_a');
const page = await browser.newPage();
const problems = [];
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
page.on('response', (r) => { if (r.status() >= 400) problems.push(`HTTP ${r.status()} ${r.url()}`); });
const feed = async (n) => {
for (let i = 0; i < n; i++) {
b.step();
const ev = b.drainEvents();
await page.evaluate((snap, ev) => {
const v = window.__demo.view;
if (ev.length) v.pushEvents({ t: 'b.ev', fieldId: snap.fieldId, gt: snap.t, ev });
v.pushSnapshot({ t: 'b.snap', gt: snap.t, ...snap });
return new Promise((r) => requestAnimationFrame(() => r()));
}, b.snapshot(), ev);
}
};
const viewOf = (id) => page.evaluate((id) => { const x = window.__demo.view.debug.views.get(id); return x ? { x: x.x, y: x.y, alive: x.alive, down: x.down ? x.down.state : null, ring: !!x._downRing?.root?.visible } : null; }, id);
const pickAt = (r, c) => page.evaluate((r, c) => {
const v = window.__demo.view, t = v.tileScreen(r, c), rc = v.debug.app.view.getBoundingClientRect();
return v.debug.pick.battleUnitAt(t.x - rc.left, t.y - rc.top)?.id ?? null;
}, r, c);
try {
await page.setViewport({ width: 1280, height: 720 });
await page.goto(`http://127.0.0.1:${srv.port}/dev/render-demo.html?scene=normal-m01&paused=1&panel=0`);
await page.waitForFunction('window.__demo && (window.__demo.ready || window.__demo.error)', { timeout: 30000 });
const meta = { ...b.fieldMeta(), fieldId: b.fieldId };
await page.evaluate((meta) => {
const v = window.__demo.view;
v.enterBattle(meta);
v.setCamera('normal', { rect: meta.rect, side: 'L', instant: true });
v.setLocalFeed({ on: true, speed: 2 });
}, meta);
await feed(20);
await new Promise((r) => setTimeout(r, 1500)); // Spine models load
// A away from its home (11,6), B lands on A's home (12,3) — a 突袭-like redeployment — and is knocked out there
b.retreat(A, { reason: 'raid' });
assert.ok(b.redeploy(A, { free: true, tile: [11, 6] }));
b.retreat(B, { reason: 'raid' });
assert.ok(b.redeploy(B, { free: true, tile: [12, 3] }));
await feed(30);
assert.deepEqual(await viewOf(B.id).then((v) => [v.x, v.y]), [3, 12], 'B stands on A\'s home');
b.kill(B);
await feed(90);
await new Promise((r) => setTimeout(r, 1200)); // the Die clip runs out (its last frame stays)
await feed(10);
const vb = await viewOf(B.id);
await page.screenshot({ path: path.join(OUT, 'feedback1f-down-home.png') });
assert.equal(vb.alive, false);
assert.equal(vb.down, 0, 'knocked down, counting');
assert.ok(vb.ring, 'its redeploy ring');
assert.deepEqual([vb.x, vb.y], [3, 10], 'drawn on its own home (10,3), not on A\'s (12,3)');
assert.equal(await pickAt(10, 3), B.id, 'a press on its own home selects it');
assert.equal(await pickAt(12, 3), null, 'A\'s home is empty');
// its timer runs out (休谟斯 70 s at 2×): it redeploys on its own home
while (!B.alive && b.time < 200) b.step();
assert.ok(B.alive, 'redeployed');
assert.deepEqual([B.tileR, B.tileC], [10, 3]);
await feed(30);
const back = await viewOf(B.id);
await page.screenshot({ path: path.join(OUT, 'feedback1f-down-redeployed.png') });
assert.equal(back.alive, true);
assert.equal(back.down, null);
assert.deepEqual([back.x, back.y], [3, 10]);
assert.deepEqual(problems, []);
} finally {
await page.close();
}
});
});