// Bond strip (active-bond discs under the top bar) and the bond detail popup: the key facts first (user playtest #2 // item 9: name, members in play / next threshold, layers, reached tier and its threshold row, the current effect with // layer-resolved numbers), then the full description and the member list with owned / on-board state — operators that // are members through 变形同构体 included (tagged 同构, gameLogic bondMembers; such a row opens the wearer's card with its // items: the pair and the granted chip), so the list's count agrees with 在场 (a bond the mode never activates says // 本局禁用 — gameLogic modeOffBonds). A core bond whose count holds 调和's +1 (the view entry's `harmony`, sent by the // server — server/match/bondsMeta.js; never re-derived here) says so: 在场 n(含调和 +1), and a 调和 row heads the member // list naming the 调和 operators on that board (gameLogic harmonyMembers: 缪尔赛思 …; a tap opens the first one's card) — // the +1 used to read as a miscount (GitHub issue #1, DESIGN §21.26). Opened from the detail panel's bond chips it docks // beside that panel (`beside`: the panel's side). // Research 06 §11.1: round mint discs, stack count over the disc, name below, sorted by stacks; grey = // present but inactive; in 联防 / boss rounds the strip is dimmed ("层数叠加已禁用"). A bond the mode never activates // that the player has members of (the server's `off` entry — server/match/bondsMeta.js offBondCounts: 标准's 投资人 奇迹 // 突袭 独行 …) comes last as a grey disc with ✕ and 本局禁用 under it instead of its count (community reports // 「投资人等在休整区就能生效的盟约不生效」 / 「…不会触发斯卡蒂与异德的突袭」: the bond just vanished, 0.1.3). The popup's 成员 // header counts the hand members too for a bond that counts the hand (投资人 远见 奇迹: gameLogic memberHeadCount), as 在场 // 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). import { html, BondDisc, Icon, MicroLabel, Tooltip } from './components.js'; import { RichText, UnitThumb, BondGlyph, GIcon } from './gameComponents.js'; import { sortBonds, bondMembers, nextThreshold, bondTier, harmonyMembers, HARMONY_BOND, memberHeadCount, briefingBondTip } from './gameLogic.js'; import { formatBondEffect } from './richText.js'; import { bondIconUrl } from './assetUrls.js'; import { data } from '../data.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); /** The "👁 name" tag of a teammate's strip (DESIGN §20.15). */ function OwnerTag({ owner }) { return html` <${GIcon} name="eye" class="bstrip__eye" /><${MicroLabel}>${owner} `; } /** * @param {{ bonds: any[], layersDisabled?: boolean, onOpen:(bondId:string)=>void, openId?: string|null, max?: number, * owner?: string|null }} props — owner: the watched teammate's name (null = your own bonds) */ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null, max = 14, owner = null }) { const sorted = sortBonds(bonds, (id) => data.lookup('bonds', id)); if (!sorted.length) { return html`
${owner ? html`<${OwnerTag} owner=${owner} />` : html`<${MicroLabel}>BONDS`}${owner ? `${owner} 尚未激活盟约` : '部署干员以激活盟约'}
`; } const m = data.get('assets'); const shown = sorted.slice(0, max); const strip = html`
${owner ? html`<${OwnerTag} owner=${owner} />` : null} ${shown.map((b) => { const rec = data.lookup('bonds', b.bondId); const th = Array.isArray(b.thresholds) && b.thresholds.length ? b.thresholds : rec?.thresholds || []; if (b.off) { // a bond the mode never activates, with members: the briefing's grey ✕ disc, no stack count, 本局禁用 under it const name = rec?.name || b.bondId; return html`
<${BondDisc} name=${name} icon=${bondIconUrl(m, b.bondId)} tier=${0} maxTier=${Math.max(1, th.length)} active=${false} disabled=${true} size="sm" showName=${true} layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)} title=${`${briefingBondTip(name, 'off')} · ${b.count ?? 0} 名成员`} /> 本局禁用
`; } const next = nextThreshold(b.count ?? 0, th); return html`
0 ? b.harmony : null} class=${cx('bslot', b.active && 'is-active', openId === b.bondId && 'is-open')}> <${BondDisc} name=${rec?.name || b.bondId} icon=${bondIconUrl(m, b.bondId)} layers=${rec?.noStack ? undefined : b.layers ?? 0} tier=${b.tier ?? 0} maxTier=${Math.max(1, th.length)} active=${!!b.active} size="sm" showName=${true} layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)} title=${`${rec?.name || b.bondId} ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'}${b.harmony > 0 ? `(含调和 +${b.harmony})` : ''}`} /> ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'}
`; })} ${sorted.length > shown.length ? html`+${sorted.length - shown.length}` : null}
`; return layersDisabled ? html`<${Tooltip} text="层数叠加已禁用" placement="bottom">${strip}` : strip; } /** * Bond detail popup. * @param {{ bondId: string, entry?: any, priv?: any, banned?: string[], onClose: Function, * onMember?: (chessId:string, items?:string[]|null)=>void, * place?: 'left'|'beside'|'besideR'|'right'|null, over?: boolean, beside?: 'left'|'right'|null, owner?: string|null, off?: boolean }} props — * `place`: where it opens (gameLogic bondPopupPlace; `beside: 'left'` = the older spelling of 'beside'); `over`: above the * detail card; `owner`: the watched teammate's name (`entry` / `priv` are then theirs: ui/watchBonds.js); `onMember` * gets a 变形同构体 row's item ids too (its card shows the pair and the granted chip), null for a plain member; `off`: the * mode never activates this bond (gameLogic modeOffBonds — 标准's 10 inactive bonds): 本局禁用 instead of 未激活, with a * note, no 在场 count and no 当前效果 block (its numbers would promise an effect the mode never gives; the bond text stays). * `entry.harmony` (the server's bond views: the +1 调和 added to `count`): 在场 n(含调和 +1) and the 调和 row */ export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember, place = null, over = false, beside = null, owner = null, off = false }) { const b = data.lookup('bonds', bondId); if (!b) return null; const count = entry?.count ?? 0; const layers = entry?.layers ?? 0; const th = Array.isArray(entry?.thresholds) && entry.thresholds.length ? entry.thresholds : b.thresholds || []; const tier = off ? 0 : entry?.tier ?? bondTier(count, th, b.maxCount); const active = !off && (entry ? !!entry.active : tier > 0); const getChess = (id) => data.lookup('chess', id); const members = bondMembers(b, priv, banned, getChess, (id) => data.lookup('items', id)); const countsHand = entry?.countsHand ?? b.countsHand; const next = nextThreshold(count, th); const hasNow = !off && !!(b.effectDescRaw || b.effectDesc); const at = place || (beside === 'left' ? 'beside' : 'left'); // 调和's +1 in this count (the server's word), and the 调和 operators on that board who give it const harmony = !off && Number.isInteger(entry?.harmony) && entry.harmony > 0 ? entry.harmony : 0; const harmonyBy = harmony ? harmonyMembers(priv, getChess) : []; const harmonyName = data.lookup('bonds', HARMONY_BOND)?.name || '调和'; const harmonyText = `${harmonyBy.length ? `${harmonyBy.map((x) => x.name).join('、')} 在场` : `${harmonyName}已激活`}:核心盟约激活人数 +${harmony}`; return html``; }