Bond strip follows the watched teammate (v2.5.2): prep scouting, after the own battle, 联防, Final Assault, eliminated observers
- the strip, its counts / layers / tiers and the bond popups show the player whose field or half is on screen, with an amber "👁 name" tag; live in-battle layers (runner bondLayers, ≤ 999) and the finished battle's gains until SETTLE - shared fields follow the ‹ › half's player; 全景 shows your own when you fight there, else the picked teammate [ASSUMED] - a card's bond chips and popups belong to the unit's owner; a teammate's popup lists their operators on the field - m.public players[].bonds for living players only; a reload while watching after the own battle returns to it - DESIGN §20.15, META, PLAYING; tests (unit, UI logic, 4 browser scenarios)
This commit is contained in:
1 parent
620f88c0d8
commit
82c64086d5
21 files changed
+2087
-57
No files matched your search
+26
-10
@@ -4,9 +4,12 @@
|
||||
// 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 ("层数叠加已禁用").
|
||||
// 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 } from './gameComponents.js';
|
||||
import { RichText, UnitThumb, BondGlyph, GIcon } from './gameComponents.js';
|
||||
import { sortBonds, bondMembers, nextThreshold, bondTier } from './gameLogic.js';
|
||||
import { formatBondEffect } from './richText.js';
|
||||
import { bondIconUrl } from './assetUrls.js';
|
||||
@@ -14,23 +17,33 @@ 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`<span class="bstrip__owner" title=${`正在查看 ${owner} 的盟约`} data-owner=${owner}>
|
||||
<${GIcon} name="eye" class="bstrip__eye" /><${MicroLabel}>${owner}</${MicroLabel}>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ bonds: any[], layersDisabled?: boolean, onOpen:(bondId:string)=>void, openId?: string|null, max?: number }} props
|
||||
* @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`<div class="bstrip bstrip--empty"><${MicroLabel}>BONDS</${MicroLabel}><span>${owner ? `${owner} 尚未激活盟约` : '部署干员以激活盟约'}</span></div>`;
|
||||
return html`<div class=${cx('bstrip', 'bstrip--empty', owner && 'is-other')} data-owner=${owner || null}>
|
||||
${owner ? html`<${OwnerTag} owner=${owner} />` : html`<${MicroLabel}>BONDS</${MicroLabel}>`}<span>${owner ? `${owner} 尚未激活盟约` : '部署干员以激活盟约'}</span></div>`;
|
||||
}
|
||||
const m = data.get('assets');
|
||||
const shown = sorted.slice(0, max);
|
||||
const strip = html`<div class=${cx('bstrip', layersDisabled && 'is-frozen', owner && 'is-other')} role="list">
|
||||
${owner ? html`<span class="bstrip__owner"><${MicroLabel}>${owner}</${MicroLabel}></span>` : null}
|
||||
const strip = html`<div class=${cx('bstrip', layersDisabled && 'is-frozen', owner && 'is-other')} role="list"
|
||||
aria-label=${owner ? `${owner} 的盟约` : '我的盟约'} data-owner=${owner || null}>
|
||||
${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 || [];
|
||||
const next = nextThreshold(b.count ?? 0, th);
|
||||
return html`<div key=${b.bondId} role="listitem" class=${cx('bslot', b.active && 'is-active', openId === b.bondId && 'is-open')}>
|
||||
return html`<div key=${b.bondId} role="listitem" data-bond=${b.bondId} class=${cx('bslot', b.active && 'is-active', openId === b.bondId && 'is-open')}>
|
||||
<${BondDisc} name=${rec?.name || b.bondId} icon=${bondIconUrl(m, b.bondId)} layers=${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)}
|
||||
@@ -46,10 +59,11 @@ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null
|
||||
/**
|
||||
* Bond detail popup.
|
||||
* @param {{ bondId: string, entry?: any, priv?: any, banned?: string[], onClose: Function, onMember?: (chessId:string)=>void,
|
||||
* place?: 'left'|'beside'|'besideR'|'right'|null, over?: boolean, beside?: 'left'|'right'|null }} props — `place`: where it
|
||||
* opens (gameLogic bondPopupPlace; `beside: 'left'` = the older spelling of 'beside'); `over`: above the detail card
|
||||
* place?: 'left'|'beside'|'besideR'|'right'|null, over?: boolean, beside?: 'left'|'right'|null, owner?: string|null }} 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)
|
||||
*/
|
||||
export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember, place = null, over = false, beside = null }) {
|
||||
export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember, place = null, over = false, beside = null, owner = null }) {
|
||||
const b = data.lookup('bonds', bondId);
|
||||
if (!b) return null;
|
||||
const count = entry?.count ?? 0;
|
||||
@@ -62,12 +76,14 @@ export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember,
|
||||
const next = nextThreshold(count, th);
|
||||
const hasNow = !!(b.effectDescRaw || b.effectDesc);
|
||||
const at = place || (beside === 'left' ? 'beside' : 'left');
|
||||
return html`<div class=${cx('bpop', 'brackets', `bpop--${at}`, over && 'is-over')} data-place=${at} role="dialog" aria-label=${`盟约:${b.name}`}>
|
||||
return html`<div class=${cx('bpop', 'brackets', `bpop--${at}`, over && 'is-over', owner && 'is-other')} data-place=${at} data-owner=${owner || null}
|
||||
role="dialog" aria-label=${owner ? `${owner} 的盟约:${b.name}` : `盟约:${b.name}`}>
|
||||
<button type="button" class="bpop__close" aria-label="关闭" onClick=${onClose}><${Icon} name="close" /></button>
|
||||
<header class="bpop__head">
|
||||
<div class=${cx('bpop__disc', active && 'is-active')}><${BondGlyph} bondId=${bondId} /></div>
|
||||
<div class="bpop__titles">
|
||||
<${MicroLabel} tone="mint">${b.isCore ? 'CORE BOND // 核心盟约' : 'ADD-ON BOND // 附加盟约'}</${MicroLabel}>
|
||||
${owner ? html`<span class="bpop__owner"><${GIcon} name="eye" /><b>${owner}</b> 的盟约</span>` : null}
|
||||
<h3 class="bpop__name">${b.name}</h3>
|
||||
<div class="bpop__facts">
|
||||
<span>在场 <b class="num">${count}</b>${next != null ? html`<small class="num">/${next}</small>` : null}${countsHand ? html`<small>(含整备区)</small>` : null}</span>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// Client-side combat (`client` prop, DESIGN §14 — the official behaviour):
|
||||
// * own normal battle over: "⌛ 作战结束,等待队友完成作战" with each teammate's live progress (kills / total, ✓);
|
||||
// * observing a teammate (team row → 前往查看): "👁 name" + 返回战场;
|
||||
// * 联防 / 最终攻势: the ‹ › pill switches the camera LEFT half / 全景 / RIGHT half ("你自己" / name / "全景");
|
||||
// * 联防 / 最终攻势: the ‹ › pill switches the camera LEFT half / 全景 / RIGHT half ("你自己" / name / "全景"); it
|
||||
// replaces the observing pill on such a field watched with 前往查看 (a 联防 leaker, an eliminated spectator — the
|
||||
// official non-helper pill "‹ 👁 helper# ›"), with 返回战场 beside the arrows (DESIGN §20.15);
|
||||
// * there is NO view switcher during normal combat.
|
||||
// Server-run combat (legacy streaming mode): the ‹ 自己 › switcher cycling the live fields of m.public.fields.
|
||||
|
||||
@@ -64,22 +66,25 @@ function ClientHud({ hud, myDone, spectating, client }) {
|
||||
if (n !== idx) client.onLayer(layers[n].key);
|
||||
};
|
||||
const observing = client.observing;
|
||||
const halves = layers.length > 0;
|
||||
return html`<div class="chud">
|
||||
<${DpCounter} dp=${hud?.dp} />
|
||||
<div class="chud__bottom">
|
||||
${myDone && !observing ? html`<div class="chud__msg chud__wait" role="status"><${Icon} name="hourglass" /><span>作战结束,等待队友完成作战</span></div>
|
||||
<${ProgressList} list=${client.progress} />` : null}
|
||||
${spectating && !observing ? html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>你已被淘汰,可点击队友头像前往查看</span></div>` : null}
|
||||
${observing ? html`<div class="vswitch chud__observe is-single" role="status">
|
||||
${observing && !halves ? html`<div class="vswitch chud__observe is-single" role="status">
|
||||
<span class="vswitch__label"><${GIcon} name="eye" /><span>${observing.name}</span></span>
|
||||
${client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
|
||||
<span class="btn__label">返回战场</span></button>` : null}
|
||||
</div>` : null}
|
||||
${!observing && layers.length ? html`<div class="vswitch chud__layers">
|
||||
${halves ? html`<div class=${cx('vswitch', 'chud__layers', observing && 'is-observing')}>
|
||||
<button type="button" class="vswitch__arrow" disabled=${idx <= 0} aria-label="左侧战场" onClick=${() => step(-1)}><${Icon} name="chevronLeft" /></button>
|
||||
<span class="vswitch__label">
|
||||
${cur && cur.watch ? html`<${GIcon} name="eye" />` : null}<span>${cur ? cur.label : '全景'}</span></span>
|
||||
<button type="button" class="vswitch__arrow" disabled=${idx >= layers.length - 1} aria-label="右侧战场" onClick=${() => step(1)}><${Icon} name="chevronRight" /></button>
|
||||
${observing && client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
|
||||
<span class="btn__label">返回战场</span></button>` : null}
|
||||
</div>` : null}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// Whose bonds the bond strip shows (DESIGN §20.15, user report after playtest #6: "观看队友时…盟约栏应该变成当前队友的
|
||||
// 盟约以及他的层数") — pure helpers for the game screen (screens/game.js), the strip and its popup (ui/bondStrip.js).
|
||||
//
|
||||
// The strip follows the player whose field / half the camera shows:
|
||||
// * the own board (prep) and the own battle → your own bonds (m.private bonds);
|
||||
// * a teammate's board in prep (前往查看, research 09 §3.1 "their board, read-only, including their pen and bonds"), a
|
||||
// teammate's battle watched after the own one ended, the field an eliminated player auto-observes → THAT player's
|
||||
// bonds (m.public players[].bonds);
|
||||
// * a shared field (联防 'u', the Final Assault / Hidden Core pair 'b1' / 'b2') [ASSUMED — no source shows the strip
|
||||
// there]: the player holding the half the ‹ › pill points at ("你自己" → yours, "👁 name" → theirs). On 全景 (or an
|
||||
// empty half, "无人在家"): yourself when you fight on that field; a viewer who does not (a 联防 leaker, an eliminated
|
||||
// spectator) never sees their own bonds there — the teammate they picked with 前往查看 (official observing targets a
|
||||
// player: ObserveUp / DeadAutoObDn { obIndex }), else the field's first player (helper 1 / the pair's first seat;
|
||||
// the official non-helper pill reads "‹ 👁 helper# ›"). 前往查看 of a player on a two-half field also moves the
|
||||
// camera to their half (playerLayer), so the pill and the strip name the same player.
|
||||
// In battle the layers are live: the local simulation of the battle on screen (the own one, a teammate's display
|
||||
// replica — battle/runner.js state().bondLayers, absolute counts ≤ BOND_LAYER_CAP) is laid over the view's start-of-
|
||||
// round counts; the server's views add the finished normal battle's gains from the end of COMBAT until the settlement
|
||||
// (server/match/bondsMeta.js bondsWithGains), so the counts never fall back during 联防.
|
||||
// A detail card's bond chips follow the UNIT's owner instead (detailBondOwner: a shared field shows both halves' units
|
||||
// whoever the strip follows), and a bond popup carries the player it was opened for (toggleBond / popupView): the
|
||||
// strip's owner, or the chip's — its entry, member list and "👁 name" label always belong to that one player.
|
||||
|
||||
import { BOND_LAYER_CAP } from '../../../shared/constants.js';
|
||||
import { sidesOf, nameOf, cameraLayers } from '../battle/observe.js';
|
||||
|
||||
const isObj = (v) => !!v && typeof v === 'object';
|
||||
const playersOf = (pub) => (Array.isArray(pub?.players) ? pub.players.filter(isObj) : []);
|
||||
const fieldsOf = (pub) => (Array.isArray(pub?.fields) ? pub.fields.filter(isObj) : []);
|
||||
|
||||
/**
|
||||
* The field on screen whose player the strip follows; null = the own prep board.
|
||||
* @param {{ combat?: boolean, settle?: boolean, watchingOther?: boolean, watching?: string|null,
|
||||
* battleFieldId?: string|null, home?: string|null }} o — battleFieldId: the battle the view shows (client-side combat:
|
||||
* the runner's battle on screen, which is also the field an eliminated player auto-observes; server-run combat: the
|
||||
* field the view entered)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function screenFieldId({ combat = false, settle = false, watchingOther = false, watching = null, battleFieldId = null, home = null } = {}) {
|
||||
if (watchingOther && typeof watching === 'string' && watching) return watching;
|
||||
if (!combat && !settle) return null;
|
||||
if (typeof battleFieldId === 'string' && battleFieldId) return battleFieldId;
|
||||
return typeof home === 'string' && home ? home : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The player whose bonds the strip shows (see the header).
|
||||
* @param {{ pub: any, myId: string, fieldId?: string|null, field?: any, layer?: 'L'|'ALL'|'R', halves?: boolean,
|
||||
* watched?: string|null }} o — field: the field meta on screen (store.match.field: `sides` / `players`), consulted when
|
||||
* it is `fieldId`; halves: the ‹ › pill is offered (battle/observe.js cameraLayers non-empty); watched: the player
|
||||
* picked with 前往查看 (watchedPlayer), consulted on a shared field the viewer does not fight on
|
||||
* @returns {string}
|
||||
*/
|
||||
export function bondOwnerId({ pub, myId, fieldId = null, field = null, layer = 'ALL', halves = false, watched = null } = {}) {
|
||||
const known = new Set(playersOf(pub).map((p) => p.playerId));
|
||||
const ok = (pid) => typeof pid === 'string' && pid !== '' && known.has(pid);
|
||||
if (typeof fieldId !== 'string' || !fieldId) return myId;
|
||||
// a normal field / a prep board: 'n:<playerId>' (DESIGN §8.3)
|
||||
if (fieldId.startsWith('n:')) {
|
||||
const pid = fieldId.slice(2);
|
||||
return ok(pid) ? pid : myId;
|
||||
}
|
||||
const meta = isObj(field) && field.fieldId === fieldId ? field : null;
|
||||
const sides = meta ? sidesOf(meta) : {};
|
||||
if (halves && meta && (layer === 'L' || layer === 'R')) {
|
||||
const pid = Object.keys(sides).find((id) => sides[id] === layer);
|
||||
if (ok(pid)) return pid;
|
||||
}
|
||||
// 全景, an empty half, no ‹ › pill: the field's members (m.public.fields, else the meta)
|
||||
const listed = fieldsOf(pub).find((f) => f.fieldId === fieldId);
|
||||
const members = Array.isArray(listed?.players) ? listed.players : Array.isArray(meta?.players) ? meta.players : [];
|
||||
if (members.includes(myId)) return myId;
|
||||
// not on that field (a 联防 leaker, an eliminated spectator): never your own — the picked teammate, else the first
|
||||
if (ok(watched) && members.includes(watched)) return watched;
|
||||
const first = members.find(ok);
|
||||
return first || myId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The player picked with 前往查看 while that watch is on screen: `who` = { fieldId, playerId } remembered by the game
|
||||
* screen when a team row was tapped (a shared field shows more than one player, the fieldId alone cannot tell).
|
||||
* @param {{ fieldId?: string, playerId?: string }|null} who @param {string|null} watching the watched fieldId
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function watchedPlayer(who, watching) {
|
||||
if (!isObj(who) || typeof who.playerId !== 'string' || !who.playerId) return null;
|
||||
return typeof watching === 'string' && watching && who.fieldId === watching ? who.playerId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ‹ › layer that shows `playerId`'s half of a two-half shared field (联防 with two helpers, a pair's boss field),
|
||||
* or null (a normal / lone field, a player not on it). 前往查看 of such a player moves the camera there.
|
||||
* @param {any} field the field meta on screen @param {any} pub @param {string} myId @param {string|null} playerId
|
||||
* @returns {'L'|'R'|null}
|
||||
*/
|
||||
export function playerLayer(field, pub, myId, playerId) {
|
||||
if (!isObj(field) || typeof playerId !== 'string' || !playerId) return null;
|
||||
if (!cameraLayers(field, pub, myId).length) return null;
|
||||
const side = sidesOf(field)[playerId];
|
||||
return side === 'L' || side === 'R' ? side : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A player's bond list as the views carry it: your own m.private bonds (thresholds / countsHand included), a teammate's
|
||||
* m.public players[].bonds ({ bondId, count, active, tier, layers }).
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function ownerBonds({ pub, priv = null, myId, ownerId }) {
|
||||
const row = (pid) => playersOf(pub).find((p) => p.playerId === pid) || null;
|
||||
if (ownerId === myId) {
|
||||
if (Array.isArray(priv?.bonds)) return priv.bonds;
|
||||
const own = row(myId)?.bonds;
|
||||
return Array.isArray(own) ? own : [];
|
||||
}
|
||||
const list = row(ownerId)?.bonds;
|
||||
return Array.isArray(list) ? list : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lay the live layers of the battle on screen over a bond list: `live` = { [bondId]: n } absolute counts (the runner's
|
||||
* bondLayers of that player). A count only ever grows in a battle — the higher of the two wins (the view may already
|
||||
* carry the settled gains) — and never shows above BOND_LAYER_CAP; a bond that gained without being listed joins the
|
||||
* list (no members, not active). Returns the list itself when there is nothing to lay over.
|
||||
* @param {any[]} bonds @param {Record<string, number>|null|undefined} live
|
||||
*/
|
||||
export function withLiveLayers(bonds, live) {
|
||||
const list = Array.isArray(bonds) ? bonds : [];
|
||||
if (!isObj(live)) return list;
|
||||
const cap = BOND_LAYER_CAP > 0 ? BOND_LAYER_CAP : Infinity;
|
||||
const ids = Object.keys(live).filter((id) => Number.isFinite(live[id]) && live[id] > 0);
|
||||
if (!ids.length) return list;
|
||||
let changed = false;
|
||||
const out = list.map((b) => {
|
||||
const n = isObj(b) && typeof b.bondId === 'string' ? live[b.bondId] : undefined;
|
||||
if (!Number.isFinite(n)) return b;
|
||||
const v = Math.min(cap, n);
|
||||
if (!(v > (Number(b.layers) || 0))) return b;
|
||||
changed = true;
|
||||
return { ...b, layers: v };
|
||||
});
|
||||
for (const id of ids) {
|
||||
if (out.some((b) => isObj(b) && b.bondId === id)) continue;
|
||||
changed = true;
|
||||
out.push({ bondId: id, count: 0, active: false, tier: 0, layers: Math.min(cap, live[id]) });
|
||||
}
|
||||
return changed ? out : list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. null without a field or battle.
|
||||
* @param {any} field @param {string} ownerId @param {any[]|null} [extra] UnitInfo-like { kind, ownerId, defId }
|
||||
*/
|
||||
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) => ({ kind: 'chess', id: u.defId }));
|
||||
return { board, hand: [], temp: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything the strip needs: whose bonds, their name (null for your own) and the list with the live layers laid over.
|
||||
* @param {{ pub: any, priv?: any, myId: string, fieldId?: string|null, field?: any, layer?: 'L'|'ALL'|'R', halves?: boolean,
|
||||
* watched?: string|null, live?: Record<string, Record<string, number>>|null }} o — live: the runner's bondLayers (null
|
||||
* outside a battle)
|
||||
* @returns {{ ownerId: string, self: boolean, name: string|null, bonds: any[] }}
|
||||
*/
|
||||
export function stripView({ pub, priv = null, myId, fieldId = null, field = null, layer = 'ALL', halves = false, watched = null, live = null }) {
|
||||
const ownerId = bondOwnerId({ pub, myId, fieldId, field, layer, halves, watched });
|
||||
const self = ownerId === myId;
|
||||
const bonds = withLiveLayers(ownerBonds({ pub, priv, myId, ownerId }), isObj(live) ? live[ownerId] : null);
|
||||
return { ownerId, self, name: self ? null : nameOf(pub, ownerId), bonds };
|
||||
}
|
||||
|
||||
/**
|
||||
* The game screen's strip from its state (screens/game.js): the field on screen, whether the ‹ › pill is offered for
|
||||
* it, the picked teammate, the live layers (battle phases and SETTLE only) — then stripView. Also returns `fieldId`.
|
||||
* @param {{ pub: any, priv?: any, myId: string, combat?: boolean, settle?: boolean, watchingOther?: boolean,
|
||||
* watching?: string|null, home?: string|null, battleFieldId?: string|null, field?: any, layers?: any[],
|
||||
* layer?: 'L'|'ALL'|'R', who?: { fieldId?: string, playerId?: string }|null,
|
||||
* bondLayers?: Record<string, Record<string, number>>|null }} o — layers: the ‹ › pill's layers on offer
|
||||
* (battle/observe.js cameraLayers of `field`, [] when not offered); bondLayers: the runner's state().bondLayers
|
||||
* @returns {{ fieldId: string|null, ownerId: string, self: boolean, name: string|null, bonds: any[] }}
|
||||
*/
|
||||
export function screenStrip({ pub, priv = null, myId, combat = false, settle = false, watchingOther = false, watching = null, home = null,
|
||||
battleFieldId = null, field = null, layers = [], layer = 'ALL', who = null, bondLayers = null }) {
|
||||
const fieldId = screenFieldId({ combat, settle, watchingOther, watching, home, battleFieldId });
|
||||
const halves = Array.isArray(layers) && layers.length > 0 && isObj(field) && field.fieldId === fieldId;
|
||||
const live = (combat || settle) && isObj(bondLayers) ? bondLayers : null;
|
||||
const watched = watchingOther ? watchedPlayer(who, watching) : null;
|
||||
return { fieldId, ...stripView({ pub, priv, myId, fieldId, field, layer, halves, watched, live }) };
|
||||
}
|
||||
|
||||
/**
|
||||
* The bonds of any player with the live layers laid over (the detail card's bond chips of a teammate's unit).
|
||||
* @param {{ pub: any, priv?: any, myId: string, ownerId: string, live?: Record<string, Record<string, number>>|null }} o
|
||||
*/
|
||||
export function playerBonds({ pub, priv = null, myId, ownerId, live = null }) {
|
||||
return withLiveLayers(ownerBonds({ pub, priv, myId, ownerId }), isObj(live) ? live[ownerId] : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whose bonds a detail card's bond chips show — and so the popup a chip opens: a unit on the field → its owner (yours or
|
||||
* a teammate's, when a known player: a 联防 / 最终攻势 field shows both halves' units whoever the strip follows); your own
|
||||
* piece (board / bench) → yours; a bond-member card opened from a popup → that popup's owner (`target.owner`); anything
|
||||
* else (shop / reward / drawer cards, a unit without a known owner) → the strip's owner.
|
||||
* @param {any} target the detail target (screens/game.js detailTarget) @param {{ pub: any, myId: string,
|
||||
* stripOwnerId?: string|null }} o
|
||||
* @returns {string}
|
||||
*/
|
||||
export function detailBondOwner(target, { pub, myId, stripOwnerId = null }) {
|
||||
const known = (pid) => typeof pid === 'string' && pid !== '' && playersOf(pub).some((p) => p.playerId === pid);
|
||||
if (isObj(target)) {
|
||||
if (target.kind === 'unit' && known(target.unit?.ownerId)) return target.unit.ownerId;
|
||||
if (target.kind === 'piece') return myId;
|
||||
if (target.kind === 'chess' && known(target.owner)) return target.owner;
|
||||
}
|
||||
return typeof stripOwnerId === 'string' && stripOwnerId ? stripOwnerId : myId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open / close a bond popup: `open` = { id, ownerId, from } — the bond, the player it shows (the strip's owner, or the
|
||||
* detail card's: detailBondOwner) and where it was opened ('strip' | 'detail'). The same bond of the same player closes it.
|
||||
* @param {{ id: string, ownerId: string, from?: string }|null} open @param {string} id @param {string} ownerId
|
||||
* @param {'strip'|'detail'} from
|
||||
*/
|
||||
export function toggleBond(open, id, ownerId, from) {
|
||||
if (isObj(open) && open.id === id && open.ownerId === ownerId) return null;
|
||||
return { id, ownerId, from };
|
||||
}
|
||||
|
||||
/**
|
||||
* The bond popup's data, for the player it was opened for (never simply the strip's: a chip of the other half's unit
|
||||
* opens THAT player's bond): their entry with the live layers (as the strip and the chips show it), the member list's
|
||||
* board (your pieces, or their operators on the field on screen — ownerBoard) and their name (null for your own).
|
||||
* @param {{ open: { id: string, ownerId?: string }|null, pub: any, priv?: any, myId: string, field?: any,
|
||||
* units?: any[]|null, live?: Record<string, Record<string, number>>|null }} o — units: the owner's operators in the
|
||||
* battle on screen (battle/runner.js ownerOps; null outside client-side combat); live: the runner's bondLayers (null
|
||||
* outside a battle)
|
||||
* @returns {{ bondId: string, ownerId: string, self: boolean, name: string|null, entry: any, priv: any }|null}
|
||||
*/
|
||||
export function popupView({ open, pub, priv = null, myId, field = null, units = null, live = null }) {
|
||||
if (!isObj(open) || typeof open.id !== 'string' || !open.id) return null;
|
||||
const ownerId = typeof open.ownerId === 'string' && open.ownerId ? open.ownerId : myId;
|
||||
const self = ownerId === myId;
|
||||
const entry = playerBonds({ pub, priv, myId, ownerId, live }).find((b) => isObj(b) && b.bondId === open.id) || null;
|
||||
return { bondId: open.id, ownerId, self, name: self ? null : nameOf(pub, ownerId), entry, priv: self ? priv : ownerBoard(field, ownerId, units) };
|
||||
}
|
||||
Reference in new issue
Block a user