diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 853e658d..c35229e5 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -137,7 +137,7 @@ - 休整期、或自己的作战结束后(显示「作战结束,等待队友完成作战」),点左侧队友头像 →「前往查看」观看他的战场;自己的作战进行中不能观战。联防和最终攻势中可以用 ‹ › 切换左半 / 全景 / 右半。 - 被淘汰后可以自由观看任何战场,而且**不用每回合重选**:你上次点「前往查看」的是谁,之后每个阶段(回合开始、机变、休整期、作战、最终攻势)都会自动跟到他的阵地或战场;没选过或他也被淘汰时,跟随座位顺序第一位还在场的博士(包括 AI 队友)。阶段中途换人立即生效,之后就跟随新选的人。 - **观战席**(本作新增,原版没有):每个同盟房间另有 2 个观战席。在大厅输入同盟密钥后点「观战」进入(模拟进行中也可以),不占博士席位、不算参战人数,也不能购买、部署、准备或发送表情;观看方式与被淘汰的玩家相同——自动跟随你上次查看的博士(没查看过则是座位顺序第一位还在场的博士):休整期显示他的阵地,作战时观看他的战场,点左侧成员头像可切换查看任何人。断线重连会回到观战席;创建者可以在等待室用观战者名字旁的 ✕ 将其移出;模拟开始后,顶栏延迟旁会出现一个「👁 人数」的小胶囊(没有观战者时不显示),点开是观战者名单(离线的有标记),创建者可以在名单里用 ✕ 把观战者移出,被移出的人立刻回到大厅、不再收到战场画面(观战席上限仍是 2 个,不能调整);观战者在等待室有空位时可以点「入座」成为博士。 -- **观看谁,盟约栏就显示谁的盟约**:查看队友的阵地或战场时(休整期、作战结束后、被淘汰后自动观战),左上的盟约栏换成**该队友的盟约、人数和层数**,前面有琥珀色的「👁 队友名」标签,点开的盟约详情也写着「队友名 的盟约」(成员一栏只标出他在场上的干员,整备区看不到);作战中层数实时增长(与他自己看到的一致)。联防和最终攻势里跟随 ‹ ›:镜头在队友的半场时显示他的盟约,在你自己的半场时显示你自己的;「全景」时,你在这个战场上作战就显示你自己的,你不在这个战场上(漏怪后观看联防、被淘汰后观战)则显示你点「前往查看」选中的那名队友,没选过就显示该战场的第一名玩家(1 号联防者 / 这一组的第一位),**不会显示你自己的**。在这样的战场上点某名队友的头像 →「前往查看」,镜头会直接移到他的半场,‹ › 上的「👁 队友名」与盟约栏的标签一致;‹ › 旁边有「返回战场」(被淘汰时没有)。盟约栏换人时,从盟约栏打开的盟约详情会自动关闭。点开场上某个干员的详情卡时,卡上的盟约(以及点它打开的盟约详情)属于**这个干员的主人**,与盟约栏当前显示谁无关——例如在「全景」点队友的干员,看到的是「队友名 的盟约」及他的人数和层数;在队友的半场点你自己的干员,看到的是你自己的。回到自己的阵地 / 战场后恢复成你自己的盟约。作战结束后观看队友时刷新页面 / 断线重连,仍会回到正在观看的队友战场,「👁 队友名」和「返回战场」都还在。已被淘汰的队友不再显示盟约(也无法被查看)。 +- **观看谁,盟约栏就显示谁的盟约**:查看队友的阵地或战场时(休整期、作战结束后、被淘汰后自动观战),左上的盟约栏换成**该队友的盟约、人数和层数**,前面有琥珀色的「👁 队友名」标签,点开的盟约详情也写着「队友名 的盟约」(成员一栏标出他在场上的干员;休整期查看时他整备区、临时整备区里的干员也会列出、标为「整备区」,和他自己看到的一样计数:投资人、远见、奇迹这类含整备区的盟约把整备区的算进成员人数,临时整备区的哪个盟约都不算;作战中只列出场上的);作战中层数实时增长(与他自己看到的一致)。联防和最终攻势里跟随 ‹ ›:镜头在队友的半场时显示他的盟约,在你自己的半场时显示你自己的;「全景」时,你在这个战场上作战就显示你自己的,你不在这个战场上(漏怪后观看联防、被淘汰后观战)则显示你点「前往查看」选中的那名队友,没选过就显示该战场的第一名玩家(1 号联防者 / 这一组的第一位),**不会显示你自己的**。在这样的战场上点某名队友的头像 →「前往查看」,镜头会直接移到他的半场,‹ › 上的「👁 队友名」与盟约栏的标签一致;‹ › 旁边有「返回战场」(被淘汰时没有)。盟约栏换人时,从盟约栏打开的盟约详情会自动关闭。点开场上某个干员的详情卡时,卡上的盟约(以及点它打开的盟约详情)属于**这个干员的主人**,与盟约栏当前显示谁无关——例如在「全景」点队友的干员,看到的是「队友名 的盟约」及他的人数和层数;在队友的半场点你自己的干员,看到的是你自己的。回到自己的阵地 / 战场后恢复成你自己的盟约。作战结束后观看队友时刷新页面 / 断线重连,仍会回到正在观看的队友战场,「👁 队友名」和「返回战场」都还在。已被淘汰的队友不再显示盟约(也无法被查看)。 ## 9. 结算称号 diff --git a/public/js/screens/game.js b/public/js/screens/game.js index 93a4e18d..7aa61f9f 100644 --- a/public/js/screens/game.js +++ b/public/js/screens/game.js @@ -1296,7 +1296,7 @@ function MatchScreen() { }, [strip.ownerId]); // the popup shows the player it was opened for (ui/watchBonds.js popupView): their entry + live layers, their name, // and as members your pieces or their operators on the field on screen — under client-side combat the battle's own - // (the runner's field meta is taken before they deploy); their hand is never sent + // (the runner's field meta is taken before they deploy); a battle sends no bench (a prep scout does: ownerBoard) const popOps = cc && battleRunner && field?.local && bondOpen && bondOpen.ownerId !== myId ? battleRunner.ownerOps(bondOpen.ownerId, field.fieldId) : null; const bondPop = popupView({ open: bondOpen, pub, priv, myId, field, units: popOps, live: liveLayers }); const openBond = (id, ownerId, from) => { setBondOpen((b) => toggleBond(b, id, ownerId, from)); audio.sfx('click', { volume: 0.4 }); }; diff --git a/public/js/ui/bondStrip.js b/public/js/ui/bondStrip.js index b656f4f4..e606bc9f 100644 --- a/public/js/ui/bondStrip.js +++ b/public/js/ui/bondStrip.js @@ -17,7 +17,9 @@ // already did. // Watching a teammate (DESIGN §20.15, ui/watchBonds.js) the strip and the popup show THAT player's bonds and layers: // the strip carries an amber "👁 name" tag (`owner`, the observing pill's spelling, research 09 §3.1) and amber rings, -// the popup a "👁 name 的盟约" line; its member list reads the teammate's operators on the field (no hand: never sent). +// the popup a "👁 name 的盟约" line; its member list reads the teammate's operators on the field — in prep their bench +// too, counted like their own popup counts it (ui/watchBonds.js ownerBoard: hand / temp by the scout's `area`, GitHub +// #385); a battle sends no bench. // 0.2.0 补位: a member the player fields as its stand-in (the own m.private.standIns; a teammate's unit that says so — // UnitInfo `standInFor`) is drawn as the stand-in — avatar, name, a small 「替补」 mark on the thumbnail — and its row // opens the stand-in's card (gameLogic memberStandIn; the owner's recall of the official mode, 2026-10-06). diff --git a/public/js/ui/watchBonds.js b/public/js/ui/watchBonds.js index cab69058..970d35ef 100644 --- a/public/js/ui/watchBonds.js +++ b/public/js/ui/watchBonds.js @@ -151,27 +151,30 @@ export function withLiveLayers(bonds, live) { * A teammate's board as far as the screen shows it, in the m.private shape the bond popup's member list reads * (gameLogic bondMembers): their operators on the field meta on screen (the prep scouting board, a server-run battle * field's units) and, under client-side combat, the battle on screen's (`extra` = battle/runner.js ownerOps: the meta - * the runner publishes is taken before the operators deploy). Their hand is never sent — members count as owned only - * when they are on the field. Their operators keep the items the unit info carries (UnitInfo `items`), so a 变形同构体 - * wearer is listed as a member of the bond it grants (gameLogic grantedBonds), and a 补位 unit its `standInFor` (the - * popup draws the stand-in). null without a field or battle. - * @param {any} field @param {string} ownerId @param {any[]|null} [extra] UnitInfo-like { kind, ownerId, defId, items?, standInFor? } + * the runner publishes is taken before the operators deploy). A prep scout also carries their bench, each unit tagged + * with its `area` (server prepFieldMeta): a 'hand' operator goes to the hand list, a 'temp' one to the temp list, so the + * popup counts them as the player's own popup does (GitHub #385, PR #387) — owned, not 在场; a hand operator counts for + * the bonds that count the 整备区 (投资人 远见 奇迹: memberHeadCount), the 5 temporary slots for none. A unit without + * an area (a battle's, an older server's) is on the board; a battle sends no bench. Their operators keep the items the + * unit info carries (UnitInfo `items`), so a 变形同构体 wearer is listed as a member of the bond it grants (gameLogic + * grantedBonds), and a 补位 unit its `standInFor` (the popup draws the stand-in). null without a field or battle. + * @param {any} field @param {string} ownerId @param {any[]|null} [extra] UnitInfo-like { kind, ownerId, defId, area?, items?, standInFor? } */ export function ownerBoard(field, ownerId, extra = null) { const fromField = isObj(field) && Array.isArray(field.units) ? field.units : null; const fromBattle = Array.isArray(extra) ? extra : null; if (!fromField && !fromBattle) return null; - const board = [...(fromField || []), ...(fromBattle || [])] - .filter((u) => isObj(u) && u.ownerId === ownerId && u.kind === 'op' && typeof u.defId === 'string') - .map((u) => { - const p = Array.isArray(u.items) && u.items.length ? { kind: 'chess', id: u.defId, items: u.items.filter((x) => typeof x === 'string') } : { kind: 'chess', id: u.defId }; - // 0.2.0 自选编队: a DIY slot's unit names its operator (UnitInfo diy) — the popup counts it for that operator's bonds - if (isObj(u.diy) && typeof u.diy.charId === 'string') p.diy = u.diy; - // 0.2.0 补位: a unit fighting as its stand-in says so (UnitInfo standInFor) — the popup draws that member as the stand-in - if (typeof u.standInFor === 'string' && u.standInFor) p.standInFor = u.standInFor; - return p; - }); - return { board, hand: [], temp: [] }; + const out = { board: /** @type {any[]} */ ([]), hand: /** @type {any[]} */ ([]), temp: /** @type {any[]} */ ([]) }; + for (const u of [...(fromField || []), ...(fromBattle || [])]) { + if (!isObj(u) || u.ownerId !== ownerId || u.kind !== 'op' || typeof u.defId !== 'string') continue; + const p = Array.isArray(u.items) && u.items.length ? { kind: 'chess', id: u.defId, items: u.items.filter((x) => typeof x === 'string') } : { kind: 'chess', id: u.defId }; + // 0.2.0 自选编队: a DIY slot's unit names its operator (UnitInfo diy) — the popup counts it for that operator's bonds + if (isObj(u.diy) && typeof u.diy.charId === 'string') p.diy = u.diy; + // 0.2.0 补位: a unit fighting as its stand-in says so (UnitInfo standInFor) — the popup draws that member as the stand-in + if (typeof u.standInFor === 'string' && u.standInFor) p.standInFor = u.standInFor; + out[u.area === 'hand' ? 'hand' : u.area === 'temp' ? 'temp' : 'board'].push(p); + } + return out; } /** diff --git a/server/match/match/views.js b/server/match/match/views.js index 6b51b37e..a6f5a13e 100644 --- a/server/match/match/views.js +++ b/server/match/match/views.js @@ -225,7 +225,8 @@ export class MatchViews { 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). */ + /** UnitInfo of a player's board pieces on their (board) tiles — a prep scout's board, the boss partner's (bossMateView). + * `area: 'board'` (a bench unit says 'hand' / 'temp': prepFieldMeta). */ _prepBoardUnits(ps) { const units = []; // the player's own view of the data (0.2.0 自选编队: its slotted DIY slots are its operators — player/diy.js) @@ -241,6 +242,7 @@ export class MatchViews { const lo = piece.kind === 'chess' && chess ? ps.loadoutFor(chess) : null; units.push({ id: piece.uid, uid: piece.uid, kind: piece.kind === 'token' ? 'token' : 'op', side: 'ally', ownerId: ps.playerId, defId: piece.id, + area: 'board', name: rec ? rec.name : piece.id, tier: rec && Number.isInteger(rec.tier) ? rec.tier : 1, golden: !!(rec && rec.isGolden), spine: assets.spine || (rec && rec.charId) || piece.id, avatar: assets.avatar || (rec && rec.charId) || piece.id, x: c, y: r, dir: pieceDir(piece), facing: pieceDir(piece) === 'LEFT' ? -1 : 1, maxHp: rec && rec.stats && Number.isFinite(rec.stats.maxHp) ? rec.stats.maxHp : 1, @@ -292,7 +294,11 @@ export class MatchViews { // (test/match/spectator.test.js). User playtest #2 item 1 (GitHub #44). // (a held chess the player fields as its stand-in is the stand-in there too — name, art, max HP — and carries // `standInFor`, like a board piece: the owner's recall of the official mode, 2026-10-06, the hand shows the stand-in) - const benchUnit = (piece, i, y) => { + // Each unit says where it waits (`area` 'hand' | 'temp'; a board unit 'board'): the bond popup of a watched player + // counts it like the player's own (ui/watchBonds.js ownerBoard — a hand operator is owned and counts for 投资人 远见 + // 奇迹, a temp one counts for nothing; GitHub #385, PR #387). The tag survives the boss-half remap (_onBossHalf), the + // row does not. + const benchUnit = (piece, i, y, area) => { const rec = piece.kind === 'item' ? gd.item(piece.id) : piece.kind === 'token' ? gd.token(piece.id) : gd.chess(piece.id); const standIn = piece.kind === 'chess' && rec && ps.fieldsStandIn(rec) ? this.gd.standIn(rec.chessId) : null; const body = standIn || rec; @@ -300,7 +306,7 @@ export class MatchViews { const lo = piece.kind === 'chess' && rec ? ps.loadoutFor(rec) : null; units.push({ id: piece.uid, uid: piece.uid, kind: piece.kind === 'token' ? 'token' : piece.kind === 'item' ? 'item' : 'op', - side: 'ally', ownerId: ps.playerId, defId: piece.id, + side: 'ally', ownerId: ps.playerId, defId: piece.id, area, name: body ? body.name : piece.id, tier: rec && Number.isInteger(rec.tier) ? rec.tier : 1, golden: !!(rec && rec.isGolden), spine: assets.spine || (body && body.charId) || piece.id, avatar: assets.avatar || (body && body.charId) || piece.id, x: i, y, maxHp: body && body.stats && Number.isFinite(body.stats.maxHp) ? body.stats.maxHp : 1, @@ -312,10 +318,10 @@ export class MatchViews { }); }; for (let i = 0; i < ps.hand.length; i++) { - if (ps.hand[i]) benchUnit(ps.hand[i], i, GEO.HAND_ROW); + if (ps.hand[i]) benchUnit(ps.hand[i], i, GEO.HAND_ROW, 'hand'); } for (let i = 0; i < ps.temp.length; i++) { - if (ps.temp[i]) benchUnit(ps.temp[i], GEO.TEMP_C0 + i, GEO.TEMP_ROW); + if (ps.temp[i]) benchUnit(ps.temp[i], GEO.TEMP_C0 + i, GEO.TEMP_ROW, 'temp'); } // `nextEnemies`: the scouted player's coming enemies — their preview pen shows on the scouting board too (research 09 // §2.2 "Teammates"; render/app.js enterBattle({ prep: true, nextEnemies })) diff --git a/test/match/feedback5-boss-pair-prep.test.js b/test/match/feedback5-boss-pair-prep.test.js index cc8b27a7..f1775eb1 100644 --- a/test/match/feedback5-boss-pair-prep.test.js +++ b/test/match/feedback5-boss-pair-prep.test.js @@ -56,6 +56,7 @@ test('a move of the partner reaches the player\'s m.private and a scout of eithe 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); @@ -63,6 +64,9 @@ test('a move of the partner reaches the player\'s m.private and a scout of eithe 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]}`])); diff --git a/test/match/prep-bench.test.js b/test/match/prep-bench.test.js index ec7e765a..baae0d12 100644 --- a/test/match/prep-bench.test.js +++ b/test/match/prep-bench.test.js @@ -37,6 +37,7 @@ test('#44 prep scout: held pieces are units on the hand row; deploying moves the const u = first.units.find((x) => x.uid === piece.uid); assert.ok(u, `${pid}: the held operator is a unit of the scout`); assert.deepEqual([u.x, u.y], [0, GEO.HAND_ROW], `${pid}: standing on the first hand slot`); + assert.equal(u.area, 'hand', 'tagged with its area: the bond popup counts it as held, not in play (GitHub #385)'); assert.equal(u.kind, 'op'); assert.equal(u.items, undefined, 'no items equipped yet'); } @@ -48,6 +49,7 @@ test('#44 prep scout: held pieces are units on the hand row; deploying moves the const last = h.lastTo('p_0', 'm.field'); const u = last.units.find((x) => x.uid === piece.uid); assert.deepEqual([u.x, u.y], [c, r], 'deployed onto the watched board'); + assert.equal(u.area, 'board'); assert.equal(last.units.some((x) => x.uid === piece.uid && x.y === GEO.HAND_ROW), false, 'and gone from the hand row'); m.dispose(); }); @@ -74,6 +76,7 @@ test('#44 prep scout: a held item is a kind-item unit on the hand row; equipped assert.ok(iu, 'the held item is a unit of the scout'); assert.equal(iu.kind, 'item'); assert.equal(iu.y, GEO.HAND_ROW); + assert.equal(iu.area, 'hand'); // equip the item onto the operator (still in the hand): the operator's unit carries the item id assert.deepEqual(m.handle('p_1', { t: 'g.equip', itemUid: item.uid, targetUid: op.uid }), { ok: true }); const after = h.lastTo('p_0', 'm.field'); @@ -109,7 +112,10 @@ test("PR #129 review: moving a hand piece to another slot is a scout change (x f const first = h.lastTo('p_0', 'm.field'); const tu = first.units.find((x) => x.uid === c.uid); assert.deepEqual([tu.x, tu.y], [GEO.TEMP_C0, GEO.TEMP_ROW], 'the temp piece scouts on the temp row (first slot, col 4)'); - assert.equal(first.units.find((x) => x.uid === a.uid).x, 0); + assert.equal(tu.area, 'temp'); + const hu = first.units.find((x) => x.uid === a.uid); + assert.equal(hu.x, 0); + assert.equal(hu.area, 'hand'); // a hand piece moved to another slot keeps uid and id — the slot must be in the signature for the push to fire assert.deepEqual(m.handle('p_1', { t: 'g.move', uid: a.uid, to: { area: 'hand', idx: 3 } }), { ok: true }); diff --git a/test/ui/watch-bonds.test.js b/test/ui/watch-bonds.test.js index bcd5f4dc..ef7411ce 100644 --- a/test/ui/watch-bonds.test.js +++ b/test/ui/watch-bonds.test.js @@ -281,16 +281,45 @@ describe('§20.15 the bonds: views + live layers', () => { assert.deepEqual(mate.bonds.map((b) => [b.bondId, b.layers]), [['sargonShip', 44], ['kjeragShip', 7]], 'THEIR bonds, THEIR live count'); assert.deepEqual(playerBonds({ pub, priv: PRIV, myId: ME, ownerId: 'p3', live }).map((b) => b.layers), [120]); }); - test('ownerBoard: the teammate\'s operators on the field on screen feed the popup\'s member list (no hand)', () => { + test('ownerBoard: the teammate\'s operators on the field on screen feed the popup\'s member list; a scout\'s bench goes to hand / temp', () => { const field = { fieldId: 'n:p2', prep: true, units: [ - { id: 1, kind: 'op', side: 'ally', ownerId: 'p2', defId: 'chess_char_1_19_a' }, - { id: 2, kind: 'token', side: 'ally', ownerId: 'p2', defId: 'tok' }, + { id: 1, kind: 'op', side: 'ally', ownerId: 'p2', defId: 'chess_char_1_19_a', area: 'board' }, + { id: 2, kind: 'token', side: 'ally', ownerId: 'p2', defId: 'tok', area: 'board' }, { id: 3, kind: 'op', side: 'ally', ownerId: 'p3', defId: 'chess_other' }, { id: 4, kind: 'enemy', side: 'enemy', ownerId: null, defId: 'enemy_1' }, + { id: 5, kind: 'op', side: 'ally', ownerId: 'p2', defId: 'chess_char_1_03_a', area: 'hand' }, + { id: 6, kind: 'op', side: 'ally', ownerId: 'p2', defId: 'chess_char_2_04_a', area: 'temp' }, + { id: 7, kind: 'item', side: 'ally', ownerId: 'p2', defId: 'chess_item_1_01_e_a', area: 'hand' }, ] }; - assert.deepEqual(ownerBoard(field, 'p2'), { board: [{ kind: 'chess', id: 'chess_char_1_19_a' }], hand: [], temp: [] }); + assert.deepEqual(ownerBoard(field, 'p2'), { + board: [{ kind: 'chess', id: 'chess_char_1_19_a' }], hand: [{ kind: 'chess', id: 'chess_char_1_03_a' }], temp: [{ kind: 'chess', id: 'chess_char_2_04_a' }], + }); + // a unit without an area (a battle's, an older server's prep scout) is on the board + assert.deepEqual(ownerBoard({ units: [{ kind: 'op', ownerId: 'p2', defId: 'chess_char_1_19_a' }] }, 'p2').board, [{ kind: 'chess', id: 'chess_char_1_19_a' }]); assert.equal(ownerBoard(null, 'p2'), null); }); + test('GitHub #385: a scouted bench counts in the popup\'s 成员 header as in the player\'s own (hand for 远见 / 奇迹 / 投资人, temp never)', () => { + // 远见 (BOARD_AND_DECK, countsHand: the server's 在场 is 2 — 赫默 on the board, 初雪 in the hand; 伊内丝 waits in temp) + // 炎 (BOARD: 在场 1 — 小满; 琳琅诗怀雅 in the hand, 烛煌 in temp) + const u = (id, defId, area) => ({ id, kind: 'op', side: 'ally', ownerId: 'p2', defId, area }); + const field = { fieldId: 'n:p2', prep: true, units: [ + u(1, 'chess_char_2_02_a', 'board'), u(2, 'chess_char_2_04_a', 'board'), + u(3, 'chess_char_3_14_a', 'hand'), u(4, 'chess_char_3_04_a', 'hand'), + u(5, 'chess_char_4_04_a', 'temp'), u(6, 'chess_char_5_03_a', 'temp'), + ] }; + const head = (bondId) => { + const v = BondPopup({ bondId, entry: bond(bondId, 0, 0, 0), priv: ownerBoard(field, 'p2'), owner: '阿米娅', onClose() {} }); + const h4 = [...walk(v)].find((x) => x.type === 'h4' && /成员/.test(textOf(x))); + const members = [...walk(v)].filter((x) => hasClass(x, 'bpop__member')); + return { head: textOf(h4).replace(/\s+/g, ''), on: members.filter((x) => hasClass(x, 'is-on')).length, owned: members.filter((x) => hasClass(x, 'is-owned')).length }; + }; + const visi = head('visiShip'); + assert.match(visi.head, /^成员2\//, '远见: the board member and the hand member (含整备区), not the temp one'); + assert.deepEqual([visi.on, visi.owned], [1, 2], 'only 赫默 is 在场; 初雪 and 伊内丝 are owned'); + const yan = head('yanShip'); + assert.match(yan.head, /^成员1\//, '炎: the board member only'); + assert.deepEqual([yan.on, yan.owned], [1, 2]); + }); test('ownerBoard: under client-side combat the battle on screen\'s operators join (the runner\'s meta predates their deploy)', () => { const meta = { fieldId: 'b1', local: true, units: [{ id: 90, kind: 'enemy', side: 'enemy', ownerId: null, defId: 'boss' }] }; const ops = [{ kind: 'op', ownerId: 'p2', defId: 'chess_char_1_09_a' }, { kind: 'op', ownerId: 'p2', defId: 'chess_char_1_03_a' }]; @@ -405,7 +434,7 @@ describe('§20.15 the popup of a card\'s bond chip: the UNIT owner\'s bond (a sh assert.deepEqual([v.entry.bondId, v.entry.count, v.entry.active, v.entry.layers], ['sargonShip', 2, true, 55], 'the partner\'s count / tier, their live layers'); assert.notEqual(v.priv, PRIV, 'not the viewer\'s pieces'); assert.deepEqual(v.priv.board.map((x) => x.id).sort(), ['chess_char_1_09_a', 'chess_char_1_12_a'], 'the partner\'s operators on the field on screen'); - assert.deepEqual([v.priv.hand, v.priv.temp], [[], []], 'their hand is never sent'); + assert.deepEqual([v.priv.hand, v.priv.temp], [[], []], 'a battle field sends no bench'); }); test('popupView: your own bond (m.private entry with thresholds, your pieces, no name); a bond not listed → no entry; nothing open → null', () => { const own = popupView({ open: { id: 'yanShip', ownerId: ME, from: 'detail' }, pub, priv: PRIV, myId: ME, field: b1, live: null });