feedback3 WD: the boss rounds' prep shows the leader on its spawn tile with its HP bar, and its hit tiles in red beside an operator's range preview (community report #12)

This commit is contained in:
sganggs committed 2026-10-04 17:42:18 +08:00
1 parent 2582367cea
commit 207a49103f
8 files changed
+282 -11

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
// test/render/feedback3-leader-prep.test.js — community report #12 (owner's decision 2026-10-04): in the Final Assault /
// Hidden Core prep the round's leader stands on the boss field (it used to wait in the off-screen pen: research 08's +13
// preview row put it in the pen's upper zone), and while an operator's orange range preview shows, the leader's hit
// tiles — the sim's hit rectangle — are lit in red [ASSUMED colour]. The match layer's preview carries the leader's spawn
// tile (server/match/waves.js previewOf `start`); render/prepfield.js leaderStand + render/pick.js hitTiles (= the sim's
// body.js bodyKeys) give the stand; render/app.js draws it (wiring: test/render/fieldview.browser.test.js). Display only.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { GameData } from '../../server/match/gamedata.js';
import { setupMatchWaves, buildBossWave, previewOf } from '../../server/match/waves.js';
import { createRng } from '../../server/sim/rng.js';
import { bodyKeys, normHitArea } from '../../server/sim/body.js';
import { COLS } from '../../server/sim/constants.js';
import { DATA } from '../match/harness.js';
import { leaderStand } from '../../public/js/render/prepfield.js';
import { hitTiles } from '../../public/js/render/pick.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
test('the preview of a boss round carries the leader\'s spawn tile; other entries never do', () => {
const p = previewOf([
{ enemyKey: 'L', count: 1, time: 0, tag: 'boss', preview: { gate: 'upper', boss: true, start: [3, 10] } },
{ enemyKey: 'a', count: 2, time: 1, preview: { gate: 'lower', start: [3, 2] } },
]);
assert.deepEqual(p[0].start, [3, 10]);
assert.equal('start' in p[1], false, 'escorts stay in the pen');
const gd = new GameData(DATA, 'mode_multi_abyss');
const setup = setupMatchWaves(gd, createRng(1));
for (const bossId of Object.keys(gd.mode.bossWeights)) {
for (const solo of [false, true]) {
const w = buildBossWave(gd, createRng(1), setup.factions, 14, { bossId, solo });
const boss = previewOf(w.spawns).filter((e) => e.boss);
assert.ok(boss.length >= 1, `${w.templateId}: a leader`);
const route = DATA.waves[w.templateId].routes[w.spawns.find((s) => s.tag === 'boss').routeIndex];
for (const e of boss) assert.deepEqual(e.start, route.start.slice(0, 2), `${w.templateId}: the leader's route start`);
}
}
});
test('leaderStand: every leader of the data stands on its spawn tile with the sim\'s hit tiles (bodyKeys)', () => {
const seen = new Set();
for (const [id, tpl] of Object.entries(DATA.waves)) {
if (!tpl || (tpl.kind !== 'boss' && tpl.kind !== 'hidden')) continue;
for (const s of tpl.spawns.filter((x) => x.tag === 'boss')) {
const start = tpl.routes[s.routeIndex].start.slice(0, 2);
const st = leaderStand([{ enemyKey: 'escort', count: 3 }, { enemyKey: s.key, count: 1, boss: true, start }], (k) => DATA.enemies[k]?.hitArea ?? null, hitTiles);
assert.ok(st, id);
assert.deepEqual([st.row, st.col], start);
const sim = bodyKeys({ x: start[1], y: start[0], hitArea: normHitArea(DATA.enemies[s.key].hitArea) }).map((k) => [Math.floor(k / COLS), k % COLS]);
assert.deepEqual(st.tiles, sim, `${id} ${s.key}: the sim's hit tiles`);
seen.add(`${s.key} ${st.tiles.length}`);
}
}
// 胄 / 管 / 昆图斯 / 阿利斯泰尔 / 萨米的意志: 5 × 3 (4.95 × 2.95, up 1); 铳 / 卢西恩 are point units: their tile
assert.ok(seen.has('enemy_9013_acstmk 15') && seen.has('enemy_9032_aclionk 15') && seen.has('enemy_9017_achunt 1'), [...seen].join(', '));
const chou = leaderStand([{ enemyKey: 'enemy_9013_acstmk', boss: true, start: [3, 10] }], (k) => DATA.enemies[k].hitArea, hitTiles);
assert.deepEqual([...new Set(chou.tiles.map(([r]) => r))], [3, 4, 5]);
assert.deepEqual([...new Set(chou.tiles.map(([, c]) => c))], [8, 9, 10, 11, 12]);
assert.equal(leaderStand([{ enemyKey: 'x', boss: true }], () => null, hitTiles), null, 'no spawn tile: it stays in the pen');
assert.equal(leaderStand([{ enemyKey: 'x', boss: false, start: [3, 10] }], () => null, hitTiles), null);
assert.equal(leaderStand(null), null);
});
test('render/app.js: the leader leaves the pen for the boss field, red hit tiles beside a range preview (source checks)', async () => {
const { LEADER_HIT_STYLE, leaderShown } = await import('../../public/js/render/app.js');
assert.equal(LEADER_HIT_STYLE.group, 'leaderHit');
assert.ok((LEADER_HIT_STYLE.color >> 16) > 0xd0 && ((LEADER_HIT_STYLE.color >> 8) & 0xff) < 0x60, 'red');
assert.equal(leaderShown('bossPrep'), true);
assert.equal(leaderShown('prep'), false);
assert.equal(leaderShown('pen', 'bossPrep'), true, 'a flight from the boss-field prep');
const app = read('public/js/render/app.js');
assert.match(app, /const pen = layoutPen\(stand \? list\.filter\(\(e\) => e !== stand\.entry\) : list, \{ stage: stageRec \}\);/);
assert.match(app, /\[\.\.\.hlReq\.keys\(\)\]\.some\(\(g\) => RANGE_GROUPS\.has\(g\)\)/);
});
test('the leader\'s view (render/app.js setLeader: a boss, not a prep view) shows its full HP bar, like the official prep image', async () => {
const { installFakePixi, fakeViewCtx } = await import('./fakepixi.js');
const { presetCamera } = await import('../../public/js/render/projection.js');
const fake = installFakePixi();
try {
const { UnitView } = await import('../../public/js/render/units.js');
const cam = presetCamera('bossPrep', { width: 1600, height: 900 });
const v = new UnitView(fakeViewCtx(fake.P, { cam: () => cam }), { id: 'leader:x', kind: 'enemy', side: 'enemy', defId: 'enemy_9013_acstmk', spine: 'enemy_9013_acstmk', tier: 3, x: 10, y: 3, facing: -1, maxHp: 1, boss: true });
v.update(1 / 60, cam, 0);
assert.equal(v.hpFill.visible, true, 'HP bar shown');
assert.ok(Math.abs(v.hpFill.width - v.hpBg.width + 2) < 1e-6, 'full');
assert.equal(v.spFill.visible, false);
v.destroy();
} finally { fake.restore(); }
});
+50 -2
View File
@@ -1,9 +1,10 @@
// test/render/fieldview.browser.test.js — field-view features in headless Chrome through the dev demo
// (public/dev/render-demo.html): the enemy preview pen (research 09 §2.2 / 08 §4.2) on the 3D and the 2D board and
// the 0.25 s pan to it and back, the Final Assault prep on the player's half of the boss field (research 09 §1.2:
// drop targets / directions stay in board space, the right half mirrored), damage numbers that never touch, the
// drop targets / directions stay in board space, the right half mirrored) with the round's leader on its spawn tile and
// its red hit tiles beside a range preview (community report #12), damage numbers that never touch, the
// automatic rebuild of a 3D board whose WebGL context was lost, and frame rates on a 4× throttled CPU.
// Screenshots → test/e2e/out/pen-*.png, fa-prep-*.png, dmgnum-*.png; perf → test/e2e/out/fieldview-perf.json.
// Screenshots → test/e2e/out/pen-*.png, fa-prep-*.png, fa-leader-*.png, dmgnum-*.png; perf → test/e2e/out/fieldview-perf.json.
//
// Opt-in (starts Chrome): RENDER_E2E=1 node --test test/render/fieldview.browser.test.js
// Run browser test files one at a time. Chrome path: $CHROME_PATH or the macOS default.
@@ -226,6 +227,53 @@ describe('field view features in headless Chrome', { skip }, () => {
});
}
for (const side of ['L', 'R']) {
test(`Final Assault prep (${side}): the leader stands on its spawn tile, out of the pen; a range preview lights its hit tiles in red (report #12)`, async () => {
const { page, problems } = await open(`scene=prep&fa=${side}&stage=act2autochess_m02`, 1920, 1080);
await wait(1500);
const r = await page.evaluate(async () => {
const v = window.__demo.view, d = v.debug;
// m.private as the server sends it in a boss round's prep: nextEnemies with the leader's spawn tile (waves.js previewOf)
const st = JSON.parse(JSON.stringify(window.__demo.scene.state));
st.nextEnemies = [
{ enemyKey: 'enemy_9013_acstmk', count: 1, gate: 'upper', t: 0, fly: false, elite: true, boss: true, source: 'wave', tag: 'boss', start: [3, 10] },
{ enemyKey: 'enemy_1007_slime', count: 3, gate: 'lower', t: 2, fly: false, elite: false, boss: false, source: 'wave', tag: null },
];
v.setPrep(st, { editable: true });
await new Promise((res) => setTimeout(res, 2000));
const L = d.leader;
const hl = () => (d.tiles.highlights.get('leaderHit')?.tiles || []).map((t) => t.join(','));
const out = { has: !!L, x: L?.view.x, y: L?.view.y, shown: !!L?.view.root.visible, facing: L?.view.visFacing, spine: !!L?.view.actor,
pen: [...d.penViews.values()].map((p) => p.info.defId), before: hl() };
v.highlightTiles([[10, 5], [10, 6], [10, 7]], { group: 'facing', color: 0xff9c33, fill: 0.36, line: 1 });
out.during = hl();
out.range = (d.tiles.highlights.get('facing')?.tiles || []).length;
return out;
});
await wait(300);
await page.screenshot({ path: path.join(OUT, `fa-leader-${side}.png`) });
const r2 = await page.evaluate(() => {
const v = window.__demo.view, d = v.debug;
v.highlightTiles([], { group: 'facing' });
const after = (d.tiles.highlights.get('leaderHit')?.tiles || []).length;
v.setCamera('prep', { instant: true });
return { after, hiddenOnBoard: !d.leader.view.root.visible };
});
await page.close();
assert.deepEqual(problems, []);
assert.deepEqual([r.has, r.x, r.y, r.shown], [true, 10, 3, true], 'on its spawn tile (3, 10), shown by the boss-field prep camera');
assert.equal(r.facing, side === 'R' ? 1 : -1, 'facing the player\'s half');
assert.ok(r.spine, 'its Spine model');
assert.ok(!r.pen.includes('enemy_9013_acstmk') && r.pen.length === 3, `not in the pen: ${r.pen}`);
assert.deepEqual(r.before, [], 'no red tiles without a range preview');
assert.equal(r.range, 3);
assert.equal(r.during.length, 15, `the 5 × 3 hit tiles: ${r.during}`);
assert.ok(['3,8', '3,12', '5,8', '5,12', '4,10'].every((t) => r.during.includes(t)));
assert.equal(r2.after, 0, 'gone with the range preview');
assert.equal(r2.hiddenOnBoard, true, 'the own-board camera does not show it');
});
}
test('damage numbers: a boss and a knot of minions under rapid mixed hits never show touching numbers', async () => {
const { page, problems } = await open('scene=numbers', 1920, 1080);
await wait(2500);