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).
114 lines
6.4 KiB
JavaScript
114 lines
6.4 KiB
JavaScript
// 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();
|
||
}
|
||
});
|
||
});
|