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:
1 parent
f14f03a59f
commit
ef10164773
16 files changed
+436
-33
No files matched your search
@@ -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
@@ -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). */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user