feedback5 CB5: the boss round's prep shows both players of a pair together — the partner's pieces on the other half, as the battle places them — and frames the pair's two avatars in the official green (community report of 2026-10-06, item 51)

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.
This commit is contained in:
sganggs committed 2026-10-07 02:29:49 +08:00
1 parent f14f03a59f
commit ef10164773
16 files changed
+436 -33

No files matched your search

+11 -2
View File
@@ -52,8 +52,17 @@ export class MatchMessaging {
}
markPublic() { this._pubDirty = true; }
/** A player's state changed: its m.private and (throttled, deduplicated) m.public (level, board, bonds…). */
markPrivate(ps) { if (ps) { this._privDirty.add(ps); this._pubDirty = true; } }
/**
* A player's state changed: its m.private and (throttled, deduplicated) m.public (level, board, bonds…) — in a boss
* round's prep also its pair partner's m.private, which shows this board on its other half (bossMate, item 51).
*/
markPrivate(ps) {
if (!ps) return;
this._privDirty.add(ps);
this._pubDirty = true;
const m = this._bossMateOf(ps);
if (m) this._privDirty.add(m.mate);
}
/** Send pending m.private (per player, only when changed) and m.public (throttled ≤ 10/s). */
flush(forcePublic = false) {
+49 -13
View File
@@ -184,17 +184,17 @@ export class MatchViews {
return previewOf([...this.wave.spawns, ...bounty]);
}
/** UnitInfo list of a player's board and hand (prep scouting): board pieces on their tiles, held pieces on the
* hand row (row 7) — the scout renders like the own prep bench. */
prepFieldMeta(ps) {
/** A 自选 piece's pick (like the sim's UnitInfo.diy): a scout's card composes the operator from it (shared/diy.js). */
_diyInfo(ps, piece) {
const p = piece.kind === 'chess' && typeof ps.diyPickOf === 'function' ? ps.diyPickOf(piece.id) : null;
return p ? { charId: p.charId, skillIndex: p.skillIndex, uniEquipId: p.uniEquipId } : undefined;
}
/** UnitInfo of a player's board pieces on their (board) tiles — a prep scout's board, the boss partner's (bossMateView). */
_prepBoardUnits(ps) {
const units = [];
// the scouted player's own view of the data (0.2.0 自选编队: its slotted DIY slots are its operators — player/diy.js)
// the player's own view of the data (0.2.0 自选编队: its slotted DIY slots are its operators — player/diy.js)
const gd = ps.gd || this.gd;
// a 自选 piece's pick (like the sim's UnitInfo.diy): the scout's card composes the operator from it (shared/diy.js)
const diyOf = (piece) => {
const p = piece.kind === 'chess' && typeof ps.diyPickOf === 'function' ? ps.diyPickOf(piece.id) : null;
return p ? { charId: p.charId, skillIndex: p.skillIndex, uniEquipId: p.uniEquipId } : undefined;
};
for (const { r, c, piece } of boardOrder(ps.board)) {
const chess = piece.kind === 'token' ? null : gd.chess(piece.id);
// 0.2.0 补位: a chess this player fields as its stand-in is deployed with the stand-in's body — name, art, max HP,
@@ -214,9 +214,42 @@ export class MatchViews {
// the equipped items (like the sim's UnitInfo): a 变形同构体 wearer shows as a member of the bond it grants
items: piece.kind === 'chess' && Array.isArray(piece.items) && piece.items.length ? piece.items.map((it) => it.id) : undefined,
standInFor: rec && rec.standInFor ? rec.standInFor : undefined,
diy: diyOf(piece),
diy: this._diyInfo(ps, piece),
});
}
return units;
}
/**
* A boss round's prep (最终攻势 / 隐秘核心: the pairing planned, no field up yet): the other player of `ps`'s pair as
* { mate: PlayerState, side: its half 'L' | 'R' }, else null (a lone player, any other phase).
*/
_bossMateOf(ps) {
if (!ps || !this.bossWaves || this.fields.length) return null;
const g = this.bossGroupOf(ps);
const pid = g && g.players.length > 1 ? g.players.find((x) => x !== ps.playerId) : null;
const mate = pid ? this.players.get(pid) : null;
return mate ? { mate, side: g.side === 'R' ? 'L' : 'R' } : null;
}
/**
* m.private `bossMate` (community report of 2026-10-06, item 51): in a boss round's prep the partner's board on its
* half of the boss field, as the battle will place it (bossFieldPlacement: the right half mirrored, RIGHT ↔ LEFT) — the
* own prep view draws it beside the own half, read-only, as the official prep shows both players of a pair together
* (the official client knows every board: ChangePositionDn { boardStatus }, research 09). Null otherwise.
* @returns {{ playerId: string, side: 'L'|'R', units: object[] } | null}
*/
bossMateView(ps) {
const m = ps && ps.alive ? this._bossMateOf(ps) : null;
if (!m) return null;
return { playerId: m.mate.playerId, side: m.side, units: this._prepBoardUnits(m.mate).map((u) => this._onBossHalf(u, m.side)) };
}
/** UnitInfo list of a player's board and hand (prep scouting): board pieces on their tiles, held pieces on the
* hand row (row 7) — the scout renders like the own prep bench. */
prepFieldMeta(ps) {
const units = this._prepBoardUnits(ps);
const gd = ps.gd || this.gd;
// the hand (整备区) and the 临时整备区 scout exactly like the own prep bench renders them: pieces as units on
// their rows (hand row 7, col = hand slot; temp row 8, cols 4..8 = temp slots; no dir — bench pieces face right),
// items included (the client draws their floating plates). PRTS 帮助 counts the temp area with the hand (review of
@@ -240,7 +273,7 @@ export class MatchViews {
moduleId: lo && typeof lo.moduleId === 'string' ? lo.moduleId : undefined,
items: piece.kind === 'chess' && Array.isArray(piece.items) && piece.items.length ? piece.items.map((it) => it.id) : undefined,
standInFor: standIn && standIn.standInFor ? standIn.standInFor : undefined,
diy: diyOf(piece),
diy: this._diyInfo(ps, piece),
});
};
for (let i = 0; i < ps.hand.length; i++) {
@@ -261,9 +294,12 @@ export class MatchViews {
// round's leader at its spawn tile (nextEnemies `start`), framed by the boss-field prep camera of the player's side
// (`side`). Until 0.2.0 an eliminated player or a spectator seat scouting a player then saw the normal board and no
// leader at all (community report of 2026-10-06, item 55).
// The pair's other player stands on the other half (item 51: both players of a pair together, as in the battle).
const g = this.fields.length ? null : this.bossGroupOf(ps);
if (g) return { ...meta, kind: 'boss', rect: { ...GEO.BOSS_RECT }, side: g.side, units: units.map((u) => this._onBossHalf(u, g.side)) };
return meta;
if (!g) return meta;
const mate = this.bossMateView(ps);
const own = units.map((u) => this._onBossHalf(u, g.side));
return { ...meta, kind: 'boss', rect: { ...GEO.BOSS_RECT }, side: g.side, units: mate ? [...own, ...mate.units] : own, ...(mate ? { mate: { playerId: mate.playerId, side: mate.side } } : {}) };
}
/** A prep UnitInfo (board, bench or temp row) placed on side 'L' | 'R' of the boss field (bossFieldPlacement). */
+10 -5
View File
@@ -240,13 +240,18 @@ export class MatchWatch {
/** Board signature of a prep scout view (board, hand and temp rows: a shop or funds change is not a board change).
* Hand / temp entries carry their slot — `prepFieldMeta` draws x from it, so a piece moved to another slot is a
* change (review of PR #129). */
* change (review of PR #129). In a boss round's prep the pair partner's board is part of the view (item 51). */
_prepScoutSig(ps) {
const parts = [];
for (const { r, c, piece } of boardOrder(ps.board)) {
const items = piece.kind === 'chess' && Array.isArray(piece.items) ? piece.items.map((it) => `${it.uid}:${it.id}`).join(',') : '';
parts.push(`${piece.uid}:${piece.id}@${r},${c}:${pieceDir(piece)}:${items}`);
}
const boardSig = (q) => {
for (const { r, c, piece } of boardOrder(q.board)) {
const items = piece.kind === 'chess' && Array.isArray(piece.items) ? piece.items.map((it) => `${it.uid}:${it.id}`).join(',') : '';
parts.push(`${piece.uid}:${piece.id}@${r},${c}:${pieceDir(piece)}:${items}`);
}
};
boardSig(ps);
const m = this._bossMateOf(ps);
if (m) { parts.push(`|mate:${m.mate.playerId}:${m.side}`); boardSig(m.mate); }
for (let i = 0; i < ps.hand.length; i++) {
const piece = ps.hand[i];
if (!piece) continue;
+6
View File
@@ -55,6 +55,8 @@ export class PlayerViews {
const free = this.shop.freeRefreshes > 0;
const board = [];
for (const { r, c, piece } of boardOrder(this.board)) board.push(this.pieceView(piece, [r, c]));
// a boss round's prep: the partner's board on its half of the boss field (Match.bossMateView, item 51)
const bossMate = typeof this.m.bossMateView === 'function' ? this.m.bossMateView(this) : null;
return {
t: 'm.private',
playerId: this.playerId,
@@ -98,6 +100,10 @@ export class PlayerViews {
// switched off)
diy: this.diy,
diyBanned: this.diyBanned,
// 最终攻势 / 隐秘核心 prep (community report of 2026-10-06, item 51): `{ playerId, side, units }` — the pair partner's
// board pieces as UnitInfo on its half of the boss field (mirrored on the right half, as the battle places them);
// the own prep view draws them beside the own half, read-only. Absent otherwise.
...(bossMate ? { bossMate } : {}),
stats: {
dmgDealt: Math.round(this.stats.dmgDealt), kills: this.stats.kills, leaks: this.stats.leaks, gold: this.stats.gold,
refreshes: this.stats.refreshes, merges: this.stats.merges,