feedback5 CB5: in a boss round's prep a watcher sees the boss field — the scouted player's half (mirrored on the right) with the leader standing at its spawn — instead of the normal board without a boss (community report of 2026-10-06, item 55)

Reproduced in the browser (notes i55-repro, an eliminated guest at the boss round): in the Final Assault fight
the watched boss field shows the leader (enemy_9013_acstmk at 10,3), but in the boss round's prep the
eliminated player saw its own empty normal board (camera 'prep', no leader), and a scout of a teammate was the
normal board too, while the players prepare on their half of the boss field with the leader standing there.
Match.prepFieldMeta: in a boss round's prep the scout is built on the boss field (kind 'boss', BOSS_RECT,
units through finalAssault.js bossFieldPlacement: rows − 7, the right half mirrored with RIGHT ↔ LEFT, the player's side;
nextEnemies keeps the leader's spawn tile); game.js frames it with the boss-field prep camera of that side,
which shows the leader (render/app.js leaderShown). With item 56 an eliminated viewer gets it at the round
start without a tap. After the fix the same run: guest field kind 'boss', camera bossPrep, leader shown.

Golden unchanged. Tests: test/match/feedback5-boss-prep-scout.test.js (new, 2). Docs: design/network.md,
design/client.md (§15).
This commit is contained in:
sganggs committed 2026-10-07 02:11:44 +08:00
1 parent e8d2b45ad4
commit f14f03a59f
5 files changed
+104 -5

No files matched your search

@@ -0,0 +1,76 @@
// Community report of 2026-10-06, item 55: 「死亡后观战视角看不到场上的关底boss」. Reproduced in the browser (an eliminated
// guest at the boss round, notes i55-repro): in the Final Assault fight the watched boss field shows the leader, but in
// the boss round's prep the eliminated player saw its own empty normal board — and a scout of a teammate (m.field
// prep) was the normal board too, while the players themselves prepare on their half of the boss field with the leader
// standing at its spawn (render/app.js setLeader, only on the boss-field prep camera). Now the scout of a boss round's
// prep is built on the boss field: the scouted player's pieces on its half (bossFieldPlacement: rows − 7, the right
// half mirrored, RIGHT ↔ LEFT), `side` for the camera, the leader in nextEnemies with its spawn tile; with item 56 an
// eliminated viewer is pushed that scout at the round start without a tap.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GEO, PHASE } from '../../shared/constants.js';
import { makeMatch, give, legalTileFor, chessOfTier } from './harness.js';
import { bossFieldPlacement } from '../../server/match/finalAssault.js';
import { viewKind, leaderShown } from '../../public/js/render/app/view.js';
import { leaderStand } from '../../public/js/render/prepfield.js';
function bossPrep(seed) {
const h = makeMatch({ mode: 'coop', difficulty: 'FUNNY', humans: 4, seed, fake: true }).start();
const m = h.m;
h.drive(() => m.phase === PHASE.PREP && m.round === m.gd.bossRound, { ready: true });
return h;
}
test('a scout of a boss round\'s prep is the boss field: the player\'s half (mirrored on the right), its side, the leader\'s spawn', () => {
const h = bossPrep(55);
const m = h.m;
assert.deepEqual(m.bossWaves.map((w) => w.players), [['p_0', 'p_1'], ['p_2', 'p_3']]);
const p1 = h.ps('p_1');
// a board piece facing RIGHT and one in the hand
for (const k of [...p1.board.keys()]) p1.board.delete(k);
const id = chessOfTier(1)[0];
const tile = legalTileFor(m, p1, id);
const onBoard = give(m, p1, id, 'board', tile);
onBoard.dir = 'RIGHT';
give(m, p1, chessOfTier(1)[1], 'hand', 2);
h.ps('p_3').eliminate(13);
assert.deepEqual(m.handle('p_3', { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
const meta = h.lastTo('p_3', 'm.field');
assert.deepEqual([meta.fieldId, meta.prep, meta.kind, meta.side], ['n:p_1', true, 'boss', 'R']);
assert.deepEqual(meta.rect, GEO.BOSS_RECT);
const u = meta.units.find((x) => x.uid === onBoard.uid);
const want = bossFieldPlacement('R', tile[0], tile[1], 'RIGHT');
assert.deepEqual([u.y, u.x, u.dir, u.facing], [want.row, want.col, 'LEFT', -1], 'mirrored onto the right half, still facing the leader');
const bench = meta.units.find((x) => x.y === 0);
assert.ok(bench && bench.x === 20 - 2, `the hand row on the half's bench row (boss row 0, mirrored): ${JSON.stringify(bench)}`);
for (const x of meta.units) assert.ok(x.y >= 0 && x.y <= 5 && x.x >= 10 && x.x <= 20, `on the right half: ${x.y},${x.x}`);
// the leader stands at its spawn tile: the client's leaderStand finds it in the scout's nextEnemies
const stand = leaderStand(meta.nextEnemies, () => null, (x, y) => [[y, x]]);
assert.ok(stand && stand.entry.boss && stand.row >= 0 && stand.row <= 5, `the leader with its spawn tile: ${JSON.stringify(stand?.entry)}`);
assert.deepEqual(meta.nextEnemies, m.nextEnemiesFor(p1), 'the scouted player\'s own preview (its pair\'s wave)');
// the client frames a scout with that rect on the boss-field prep camera, which shows the leader
assert.equal(viewKind('prep', { rect: meta.rect, side: meta.side }), 'bossPrep');
assert.equal(leaderShown('bossPrep'), true);
// the left player of the pair: its own half, unmirrored
assert.deepEqual(m.handle('p_3', { t: 'g.watch', fieldId: 'n:p_0' }), { ok: true });
const left = h.lastTo('p_3', 'm.field');
assert.deepEqual([left.kind, left.side], ['boss', 'L']);
for (const x of left.units) assert.ok(x.x <= 10, 'the left half');
m.dispose();
});
test('the eliminated viewer is pushed the boss-field scout at the boss round\'s start (item 56); a normal round\'s scout stays the normal board', () => {
const h = makeMatch({ mode: 'coop', difficulty: 'FUNNY', humans: 3, seed: 56, fake: true }).start();
const m = h.m;
h.toPrep(1);
h.ps('p_2').eliminate(1);
h.toPrep(2);
const normal = h.lastTo('p_2', 'm.field');
assert.deepEqual([normal.fieldId, normal.kind, normal.prep, normal.side], ['n:p_0', 'normal', true, undefined]);
assert.deepEqual(normal.rect, GEO.NORMAL_RECT);
h.drive(() => m.phase === PHASE.PREP && m.round === m.gd.bossRound, { ready: true });
const meta = h.lastTo('p_2', 'm.field');
assert.deepEqual([meta.fieldId, meta.kind, meta.prep, meta.side], ['n:p_0', 'boss', true, 'L']);
assert.ok(leaderStand(meta.nextEnemies, () => null, (x, y) => [[y, x]]), 'the leader at its spawn');
m.dispose();
});