GitHub #385 / PR #387 by @zasx77771. The prep scout sends the hand and the 临时整备区 as units, and ownerBoard put every op unit on the popup's synthetic board, so the popup marked bench operators 在场 and its 成员 header disagreed with the server's ring (炎: ring 1, header 3; 远见: ring 2, header 3 with the temp copy). The scout's units now carry `area` 'board' | 'hand' | 'temp' (the server tags from PR #387; the tag survives the boss-half remap, the row does not). ownerBoard sorts them into board / hand / temp instead of dropping the bench as the PR did: dropping the hand would break the bonds that count the 整备区 (投资人 / 远见 / 奇迹 — a hand-only 远见 member: ring 1, header 0). bondMembers / memberHeadCount then count exactly as the player's own popup: hand = owned and in hand, temp = owned only. A unit without an area (battles, older servers) stays on the board. Docs: PLAYING.md's 观看谁 paragraph (the bench is listed in prep); comments in bondStrip.js / game.js. Tests: test/ui/watch-bonds.test.js (36; 2 fail on the parent), test/match/prep-bench.test.js + feedback5-boss-pair-prep.test.js (area tags; 4 fail on the parent). Golden unchanged (views only).
121 lines
7.9 KiB
JavaScript
121 lines
7.9 KiB
JavaScript
// Community report of 2026-10-06, item 51: 「还原原版最终boss战准备阶段:像战斗中那样把会成一组的两个玩家放在一起(靠右边的
|
|
// 那个他自己的视角就是靠右,他之前的摆位也镜像过去,干员位置面向相对boss是不变的),同组两个玩家屏幕左边的头像也像原版那样用绿框
|
|
// 框起来」. The own half (mirrored for the right-hand player, RIGHT ↔ LEFT) and its camera were already the official ones
|
|
// (research 09 §1.2, DESIGN §15); missing were the partner's pieces on the other half and the frames. Now m.private
|
|
// carries `bossMate` in a boss round's prep — the partner's board on its half as the battle places it — kept current
|
|
// when the partner moves (the partner's m.private is marked with the mover's), a scout of either player shows both
|
|
// halves, and the team panel frames the pair (gameLogic teamFrameIds). Overlap: PR #191 (same-field frames, issue #190)
|
|
// published the pairing as m.public bossPairing and framed the partner alone in gold — not merged; here the client reads
|
|
// the pairing as prepCamera already does, and both avatars get the official green frame (ui/battle bg_team_border).
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { PHASE } from '../../shared/constants.js';
|
|
import { makeMatch, give, legalTileFor, chessOfTier } from './harness.js';
|
|
import { bossFieldPlacement } from '../../server/match/finalAssault.js';
|
|
import { teamFrameIds } from '../../public/js/ui/gameLogic.js';
|
|
|
|
function bossPrep(humans, seed) {
|
|
const h = makeMatch({ mode: 'coop', difficulty: 'FUNNY', humans, seed, fake: true }).start();
|
|
const m = h.m;
|
|
h.drive(() => m.phase === PHASE.PREP && m.round === m.gd.bossRound, { ready: true });
|
|
for (const ps of m.players.values()) { ps.ready = false; for (const k of [...ps.board.keys()]) ps.board.delete(k); ps.recompute(); }
|
|
m.flush(true);
|
|
return h;
|
|
}
|
|
const place = (h, pid, id) => {
|
|
const ps = h.ps(pid);
|
|
const tile = legalTileFor(h.m, ps, id);
|
|
const piece = give(h.m, ps, id, 'board', tile);
|
|
piece.dir = 'RIGHT';
|
|
ps.recompute();
|
|
return { piece, tile };
|
|
};
|
|
|
|
test('m.private bossMate: each player of a pair gets the other\'s board on its half — mirrored on the right, facing kept', () => {
|
|
const h = bossPrep(4, 5101);
|
|
const m = h.m;
|
|
assert.deepEqual(m.bossWaves.map((w) => w.players), [['p_0', 'p_1'], ['p_2', 'p_3']]);
|
|
const a = place(h, 'p_0', chessOfTier(1)[0]);
|
|
const b = place(h, 'p_1', chessOfTier(1)[1]);
|
|
m.flush(true);
|
|
const v0 = h.ps('p_0').privateView();
|
|
const v1 = h.ps('p_1').privateView();
|
|
assert.deepEqual([v0.bossMate.playerId, v0.bossMate.side], ['p_1', 'R']);
|
|
assert.deepEqual([v1.bossMate.playerId, v1.bossMate.side], ['p_0', 'L']);
|
|
const onR = v0.bossMate.units.find((u) => u.uid === b.piece.uid);
|
|
const wantR = bossFieldPlacement('R', b.tile[0], b.tile[1], 'RIGHT');
|
|
assert.deepEqual([onR.y, onR.x, onR.dir, onR.facing, onR.ownerId], [wantR.row, wantR.col, 'LEFT', -1, 'p_1'], 'the right half: mirrored, still facing the leader');
|
|
const onL = v1.bossMate.units.find((u) => u.uid === a.piece.uid);
|
|
const wantL = bossFieldPlacement('L', a.tile[0], a.tile[1], 'RIGHT');
|
|
assert.deepEqual([onL.y, onL.x, onL.dir, onL.ownerId], [wantL.row, wantL.col, 'RIGHT', 'p_0'], 'the left half as it is');
|
|
assert.equal(v0.bossMate.units.length, 1, 'the partner\'s board only (its bench stays its own)');
|
|
m.dispose();
|
|
});
|
|
|
|
test('a move of the partner reaches the player\'s m.private and a scout of either player at once', () => {
|
|
const h = bossPrep(4, 5102);
|
|
const m = h.m;
|
|
const b = place(h, 'p_1', chessOfTier(1)[1]);
|
|
const held = give(m, h.ps('p_0'), chessOfTier(1)[2]); // into p_0's hand
|
|
m.flush(true);
|
|
// p_3 (eliminated) scouts p_0: the scout shows p_0's half and p_1's board on the other half
|
|
h.ps('p_3').eliminate(13);
|
|
assert.deepEqual(m.handle('p_3', { t: 'g.watch', fieldId: 'n:p_0' }), { ok: true });
|
|
let scout = h.lastTo('p_3', 'm.field');
|
|
assert.deepEqual([scout.kind, scout.side, scout.mate], ['boss', 'L', { playerId: 'p_1', side: 'R' }]);
|
|
assert.ok(scout.units.some((u) => u.uid === b.piece.uid && u.x >= 10), 'the partner\'s piece on the right half');
|
|
// the area tags survive the boss-half remap (the rows do not): the bond popup tells the bench from the board (GitHub #385)
|
|
assert.equal(scout.units.find((u) => u.uid === b.piece.uid).area, 'board');
|
|
assert.equal(scout.units.find((u) => u.uid === held.uid).area, 'hand', 'a held operator stays tagged as held');
|
|
// p_1 moves its piece: p_0's m.private and p_3's scout of p_0 follow
|
|
const ps1 = h.ps('p_1');
|
|
const to = legalTileFor(m, ps1, b.piece.id, new Set([`${b.tile[0]},${b.tile[1]}`]));
|
|
const before = h.allTo('p_3', 'm.field').length;
|
|
assert.deepEqual(m.handle('p_1', { t: 'g.move', uid: b.piece.uid, to: { area: 'board', row: to[0], col: to[1] }, dir: 'UP' }), { ok: true });
|
|
m.flush(true);
|
|
const want = bossFieldPlacement('R', to[0], to[1], 'UP');
|
|
const u0 = h.lastTo('p_0', 'm.private').bossMate.units.find((u) => u.uid === b.piece.uid);
|
|
assert.deepEqual([u0.y, u0.x, u0.dir], [want.row, want.col, 'UP']);
|
|
assert.ok(h.allTo('p_3', 'm.field').length > before, 'the scout of p_0 was pushed again');
|
|
scout = h.lastTo('p_3', 'm.field');
|
|
const us = scout.units.find((u) => u.uid === b.piece.uid);
|
|
assert.deepEqual([us.y, us.x, us.dir], [want.row, want.col, 'UP']);
|
|
m.dispose();
|
|
});
|
|
|
|
test('no bossMate for a lone player, outside a boss round\'s prep, or once the fight is on', () => {
|
|
const h = bossPrep(3, 5103);
|
|
const m = h.m;
|
|
assert.deepEqual(m.bossWaves.map((w) => w.players), [['p_0', 'p_1'], ['p_2']]);
|
|
assert.equal(h.ps('p_2').privateView().bossMate, undefined, 'the lone player of a 3-player Final Assault');
|
|
assert.ok(h.ps('p_0').privateView().bossMate, 'the pair has one');
|
|
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
|
assert.equal(h.ps('p_0').privateView().bossMate, undefined, 'the fight: the boss field itself is shown');
|
|
m.dispose();
|
|
const h2 = makeMatch({ mode: 'coop', humans: 2, seed: 5104, fake: true }).start();
|
|
h2.toPrep(1);
|
|
assert.equal(h2.ps('p_0').privateView().bossMate, undefined, 'a normal round');
|
|
h2.m.dispose();
|
|
});
|
|
|
|
test('client: teamFrameIds frames the viewer\'s pair in a boss round (prep and fight), nobody otherwise', () => {
|
|
const players = [0, 1, 2, 3].map((i) => ({ playerId: `p_${i}`, seat: i, alive: true }));
|
|
const pub = (o) => ({ round: 14, bossRound: 14, hiddenRound: 15, phase: PHASE.PREP, players, fields: [], ...o });
|
|
const ids = (s) => [...s].sort();
|
|
assert.deepEqual(ids(teamFrameIds(pub({}), 'p_0')), ['p_0', 'p_1']);
|
|
assert.deepEqual(ids(teamFrameIds(pub({}), 'p_3')), ['p_2', 'p_3']);
|
|
assert.deepEqual(ids(teamFrameIds(pub({ phase: PHASE.ROUND_START }), 'p_2')), ['p_2', 'p_3'], 'from the round start');
|
|
assert.deepEqual(ids(teamFrameIds(pub({ phase: PHASE.SP_DRAFT, round: 15 }), 'p_1')), ['p_0', 'p_1'], 'the Hidden Core prep');
|
|
// seat 1 out: the players still in pair by seat (p_0, p_2), p_3 alone
|
|
const out = players.map((p) => (p.playerId === 'p_1' ? { ...p, alive: false } : p));
|
|
assert.deepEqual(ids(teamFrameIds(pub({ players: out }), 'p_2')), ['p_0', 'p_2']);
|
|
assert.deepEqual(ids(teamFrameIds(pub({ players: out }), 'p_3')), [], 'a lone player: no frame');
|
|
assert.deepEqual(ids(teamFrameIds(pub({ players: out }), 'p_1')), [], 'an eliminated viewer: no pair');
|
|
assert.deepEqual(ids(teamFrameIds(pub({ round: 13 }), 'p_0')), [], 'a normal round');
|
|
assert.deepEqual(ids(teamFrameIds(pub({}), 's_spec')), [], 'a spectator seat');
|
|
const fight = pub({ phase: PHASE.FINAL_ASSAULT, fields: [{ fieldId: 'b1', kind: 'boss', players: ['p_0', 'p_1'] }, { fieldId: 'b2', kind: 'boss', players: ['p_2'] }] });
|
|
assert.deepEqual(ids(teamFrameIds(fight, 'p_1')), ['p_0', 'p_1'], 'the fight: the boss field listing the viewer');
|
|
assert.deepEqual(ids(teamFrameIds(fight, 'p_2')), [], 'a lone boss field');
|
|
assert.deepEqual(ids(teamFrameIds(pub({ phase: PHASE.UNITE, round: 12 }), 'p_0')), [], '联防 is not framed (the owner asked for the boss rounds)');
|
|
});
|