The own half was already the official one (the right-hand player on the right half with the
right_boss_*_camera_param camera, its layout mirrored col c -> 20 - c with RIGHT <-> LEFT, so its operators
face the leader as before: research 09 §1.2, DESIGN §15); the partner's half stood empty and nothing framed
the pair.
Server: in a boss round's prep m.private.bossMate { playerId, side, units } = the partner's board as UnitInfo
on its half (Match.bossMateView, finalAssault.js bossFieldPlacement); markPrivate marks the partner's
m.private with the mover's, so it follows live; a scout of either player (Match.prepFieldMeta) carries both
halves (`mate`), and its signature covers the partner's board.
Client: render/app.js syncMates draws the partner's units read-only ('m:<uid>' views: never picked, dragged
or swept with the own pieces) and the whole boss field is lit; the team panel frames the pair's avatars
(gameLogic teamFrameIds: the players still in by seat, two by two, as prepCamera — the boss field in the
fight) from the round start through the Final Assault / Hidden Core, with the official ui/battle
bg_team_border tinted the autochess green (#4ed8af; a plain ring without the local art); none for a lone
player, an eliminated player, a spectator seat, or 联防.
Overlap with PR #191 (same-field frames, issue #190; not merged): it published m.public bossPairing and
framed the partner alone in gold, 联防 included, plus a banner naming the partner; here the pairing is read
as the prep camera already does, both avatars get the official green frame, boss rounds only.
[ASSUMED] the partner's bench is not shown; one frame per avatar.
Golden unchanged. Tests: test/match/feedback5-boss-pair-prep.test.js (new, 4), the item 55 test now checks
the scouted player's own pieces (the partner's stand on the other half); SP_E2E
test/ui/feedback5-boss-pair.e2e.test.js (new, 2: both halves and the frames in prep and fight; item 55's
eliminated guest on the boss field with the leader). Docs: design/client.md §10 / §15, design/network.md,
PLAYING.md.
79 lines
5.2 KiB
JavaScript
79 lines
5.2 KiB
JavaScript
// 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.filter((x) => x.ownerId === 'p_1')) 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']);
|
||
// its own pieces on the left half (since item 51 the partner p_1's board stands on the right half beside them)
|
||
for (const x of left.units.filter((x) => x.ownerId === 'p_0')) assert.ok(x.x <= 10, 'the left half');
|
||
assert.ok(left.units.some((x) => x.ownerId === 'p_1' && x.uid === onBoard.uid && x.x === want.col), 'p_1\'s piece on the right 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();
|
||
});
|