diff --git a/public/css/screens/game-panels.css b/public/css/screens/game-panels.css
index aee835ec..ffc75b31 100644
--- a/public/css/screens/game-panels.css
+++ b/public/css/screens/game-panels.css
@@ -38,6 +38,9 @@
.uthumb.is-golden::after { content: ''; position: absolute; inset: -2px; border: 1px solid rgba(255, 198, 0, .7); pointer-events: none; clip-path: inherit; }
.uthumb.is-dim .uthumb__art img { filter: grayscale(.85) brightness(.55); }
.uthumb__tag { position: absolute; right: 0; bottom: 0; font-size: calc(var(--ut) * .17); line-height: 1.2; padding: 0 .03rem; background: #a9c3d8; color: #0b1520; font-weight: 700; }
+/* 0.2.0 补位: the small 「替补」 mark of a stand-in's thumbnail (the result lineup, bond popup members — screens/result.js,
+ ui/bondStrip.js) */
+.uthumb__si { position: absolute; right: 0; bottom: 0; font-size: max(calc(var(--ut) * .2), 8px); line-height: 1.2; padding: 0 .03rem; background: var(--ice); color: var(--ice-900); font-weight: 700; opacity: .9; pointer-events: none; }
.bglyph { display: inline-grid; place-items: center; width: .24rem; height: .24rem; flex: none; }
.bglyph img { width: 100%; height: 100%; object-fit: contain; filter: brightness(0) invert(.86) sepia(.4) hue-rotate(110deg) saturate(2.5); }
@@ -82,6 +85,8 @@
.dhead__chips { display: flex; align-items: center; gap: .08rem; flex-wrap: wrap; }
.dhead__name { font-size: .32rem; font-weight: 900; letter-spacing: .04em; line-height: 1.2; }
.dhead__en { font-family: var(--font-display); font-size: .12rem; letter-spacing: .2em; color: var(--text-dim); text-transform: uppercase; }
+/* 0.2.0 补位: the replaced operator under a stand-in's name (「银灰的替补」, ui/detailPanel.js ChessDetail) */
+.dhead__for { display: block; font-size: max(.13rem, 9px); letter-spacing: .04em; color: var(--ice); }
.dhead__flavor { font-size: .13rem; color: var(--text-lo); font-style: italic; }
.dhead__class { display: flex; align-items: center; gap: .06rem; flex-wrap: wrap; font-size: .15rem; color: var(--text-md); margin-top: .06rem; }
.dhead__class .sep { width: 1px; height: .16rem; background: var(--line-2); }
@@ -90,9 +95,9 @@
.dtag-elite, .dtag-token, .dtag-kind { font-size: .13rem; font-weight: 700; padding: .01rem .08rem; letter-spacing: .08em; }
.dtag-elite { background: var(--gold); color: #1d1400; }
.dtag-token { background: #a9c3d8; color: #0b1520; }
-/* 0.2.0 补位: the card of a chess fielded as its stand-in (ui/detailPanel.js ChessDetail) */
+/* 0.2.0 自选编队 / 补位: the card of a 自选 piece, of a chess fielded as its stand-in (ui/detailPanel.js ChessDetail) */
.dtag-diy { font-size: .13rem; font-weight: 700; padding: .01rem .08rem; letter-spacing: .08em; background: var(--mint-500); color: var(--text-on-mint); }
-.dtag-standin { font-size: .13rem; font-weight: 700; padding: .01rem .08rem; letter-spacing: .04em; background: var(--ice); color: var(--ice-900); }
+.dtag-standin { font-size: .13rem; font-weight: 700; padding: .01rem .08rem; letter-spacing: .08em; background: var(--ice); color: var(--ice-900); }
.dtag-kind { border: 1px solid var(--line-2); color: var(--text-lo); font-weight: 500; }
.drank { font-size: .13rem; font-weight: 700; padding: .01rem .08rem; }
.drank--normal { background: var(--bg-4); color: var(--text-md); }
diff --git a/public/css/screens/game-shop.css b/public/css/screens/game-shop.css
index 3ac8c551..f62b8ba1 100644
--- a/public/css/screens/game-shop.css
+++ b/public/css/screens/game-shop.css
@@ -130,9 +130,10 @@
.scard__class { display: flex; align-items: center; gap: .05rem; font-size: .13rem; color: var(--text-lo); }
.scard__prof { width: .18rem; height: .18rem; object-fit: contain; opacity: .85; }
.scard__mergetag { position: absolute; z-index: 3; right: 0; top: .38rem; font-size: .12rem; font-weight: 700; padding: .01rem .06rem; background: var(--gold); color: #1d1400; letter-spacing: .08em; }
-/* 0.2.0 补位: a chess the player does not own — its card stays, the stand-in fights (ui/shopBar.js ChessCard) */
-.scard__standin { position: absolute; z-index: 3; left: 0; top: .38rem; max-width: calc(100% - .1rem); font-size: .12rem; font-weight: 700; padding: .01rem .06rem;
- background: var(--ice); color: var(--ice-900); letter-spacing: .04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; box-shadow: 0 1px 3px rgba(0, 0, 0, .6); }
+/* 0.2.0 补位: a chess the player does not own shows its stand-in (ui/shopBar.js ChessCard) — a small 「替补」 mark */
+.scard__standin { position: absolute; z-index: 3; left: 0; top: .38rem; max-width: calc(100% - .1rem); font-size: max(.11rem, 8px); font-weight: 700; line-height: 1.4;
+ padding: 0 .05rem; background: var(--ice); color: var(--ice-900); letter-spacing: .08em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ opacity: .9; box-shadow: 0 1px 3px rgba(0, 0, 0, .6); }
/* 0.2.0 自选编队: a DIY slot the player filled — the card is its operator, only in this player's shop (ui/shopBar.js) */
.scard__diy { position: absolute; z-index: 3; left: 0; top: .38rem; font-size: .12rem; font-weight: 700; padding: .01rem .06rem;
background: var(--mint-500); color: var(--text-on-mint); letter-spacing: .08em; white-space: nowrap; box-shadow: 0 1px 3px rgba(0, 0, 0, .6); }
diff --git a/public/i18n/en.json b/public/i18n/en.json
index 53291a16..727fe090 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -875,6 +875,10 @@
"自选": "Custom",
"自选编队:所选技能与模组,没有特质,盟约按所属阵营分配": "Custom Squad: the chosen skill and module, no Attribute, Alliances by its factions",
"自选编队:{name}": "Custom Squad: {name}",
+ "替补": "Stand-in",
+ "{name}的替补": "Stand-in for {name}",
+ "未持有{name}:由替补干员 {standIn} 上场(盟约、特质、阶级与价格不变)": "{name} not owned: the stand-in {standIn} takes the field (Alliances, Attribute, tier and price unchanged)",
+ "未持有的干员由官方指定的替补干员上场:商店、整备区、作战区与结算都显示替补干员(模型、技能、天赋、攻击范围),盟约、特质、阶级、价格与合成仍按原干员。预设干员无论是否持有都由本人上场,不在此列。": "An Operator you do not own takes the field as its official stand-in: the shop, the Bench, the battlefield and the results show the stand-in (model, skills, talents, attack range), while Alliances, Attribute, tier, price and promotion follow the original Operator. Preset Operators always take the field themselves, owned or not, and are not listed.",
"全部清空": "Clear All",
"清空全部 {n} 个自选名额?": "Clear all {n} Custom Squad {n|slot|slots}?",
"文件过大,请选择「导出」下载的自选编队文件": "The file is too large. Choose a Custom Squad file downloaded with \"Export\".",
diff --git a/public/js/battle/runner.js b/public/js/battle/runner.js
index 38b58468..57e580a5 100644
--- a/public/js/battle/runner.js
+++ b/public/js/battle/runner.js
@@ -55,9 +55,9 @@
// 0.1.0). Read-only: it takes the stats the sim computed last (`unit._s`) and the range grid it
// keeps, and never makes the unit recompute them, so looking never changes the battle's floats.
// battleRunner.unitIdOf(uid, ownerId, fieldId?) → the id of an own board piece's unit in that battle | null
-// battleRunner.ownerOps(ownerId, fieldId?) → [{ kind: 'op', ownerId, defId, items? }] that player's operators in the
-// battle on screen with their equipment (a teammate's bond popup: the members in play, DESIGN
-// §20.15, 变形同构体 wearers included) | []
+// battleRunner.ownerOps(ownerId, fieldId?) → [{ kind: 'op', ownerId, defId, items?, standInFor? }] that player's
+// operators in the battle on screen with their equipment (a teammate's bond popup: the members in
+// play, DESIGN §20.15, 变形同构体 wearers included; a 补位 stand-in names the replaced charId) | []
//
// createBattleRunner(deps) builds an instance with injectable net / store / clock / frame scheduler / sim loader
// (test/match/runner.test.js drives it under Node).
@@ -793,7 +793,11 @@ export function createBattleRunner(deps) {
if (!e || typeof ownerId !== 'string' || !ownerId || (fieldId != null && e.fieldId !== fieldId)) return [];
const list = Array.isArray(e.battle.allyUnits) ? e.battle.allyUnits : [];
return list.filter((u) => u && u.kind === 'op' && u.ownerId === ownerId && typeof u.defId === 'string')
- .map((u) => (Array.isArray(u.items) && u.items.length ? { kind: 'op', ownerId, defId: u.defId, items: [...u.items] } : { kind: 'op', ownerId, defId: u.defId }));
+ .map((u) => {
+ const o = Array.isArray(u.items) && u.items.length ? { kind: 'op', ownerId, defId: u.defId, items: [...u.items] } : { kind: 'op', ownerId, defId: u.defId };
+ const si = u.def && typeof u.def.standInFor === 'string' ? u.def.standInFor : null;
+ return si ? { ...o, standInFor: si } : o;
+ });
},
/** Re-show the current battle (the game screen remounted). */
reshow() { if (cur) show(cur); },
diff --git a/public/js/render/app.js b/public/js/render/app.js
index a4f86610..2e33cb06 100644
--- a/public/js/render/app.js
+++ b/public/js/render/app.js
@@ -275,7 +275,7 @@ export async function createFieldView(host, options = {}) {
let penHidden = true; // the pen's figures are shown only by the pen camera (setPenHidden)
let penList = null;
let ownPen = null; // the own m.private.nextEnemies (fallback composition of a scouted teammate's pen)
- let standInList = []; // the own m.private.standIns (0.2.0 补位): board pieces of these chess draw the stand-in
+ let standInList = []; // the own m.private.standIns (0.2.0 补位): pieces of these chess draw the stand-in
let diyPicks = {}; // the own m.private.diy (0.2.0 自选编队): pieces of these DIY slots draw the operator
let camBeforePen = null; // { kind, opts } the camera the pen returns to
let leader = null; // { key, view, stand, area } the round leader standing on the boss field in the prep (setLeader)
@@ -631,9 +631,9 @@ export async function createFieldView(host, options = {}) {
return { kind: 'token', side: 'ally', defId: piece.id, spine: rec?.assets?.spine || piece.id, avatar: rec?.assets?.avatar || piece.id, tier: piece.tier || 1, golden: false, dir };
}
const chess = data.chess(piece.id);
- // 0.2.0 补位: a board piece of a chess the player does not own (m.private.standIns) is deployed as its stand-in —
- // the stand-in's model; on the bench it keeps the chess's own (the hand shows the original operator)
- const si = area === 'board' && chess && standInList.includes(chess.baseId || chess.chessId) ? data.standIn(piece.id) : null;
+ // 0.2.0 补位: a piece of a chess the player does not own (m.private.standIns) is its stand-in's model, in the hand,
+ // the 临时整备区 and on the board alike (the owner's recall of the official mode, 2026-10-06)
+ const si = chess && standInList.includes(chess.baseId || chess.chessId) ? data.standIn(piece.id) : null;
// 0.2.0 自选编队: a DIY slot the player filled is its operator, on the board and on the bench alike
const pick = chess && chess.isDiy ? diyPicks[chess.baseId || chess.chessId] : null;
const dr = pick ? data.diy(piece.id, pick) : null;
@@ -720,7 +720,7 @@ export async function createFieldView(host, options = {}) {
const key = 'p:' + e.uid;
e.key = key;
const info = pieceInfo(e.piece, e.area);
- // (the model is part of it: a 补位 chess changes body between the bench and the board — pieceInfo)
+ // (the model is part of it: a piece whose body changes — a merge, an own 补位 / 自选 setting arriving — is rebuilt)
const sig = `${info.kind}|${info.defId}|${info.golden ? 1 : 0}|${info.spine || ''}`;
let v = views.get(key);
if (v && v._sig !== sig) { dropView(key); v = null; }
diff --git a/public/js/screens/game.js b/public/js/screens/game.js
index a2e0dd1c..bc03e9b2 100644
--- a/public/js/screens/game.js
+++ b/public/js/screens/game.js
@@ -96,7 +96,7 @@ import {
snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, sortedPlayers,
countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason,
previewEnemyKey, prepCamera, prepCameraFor, foldCamera, deployFieldOf, panelSide, panelSlots, bondPopupPlace, unitLoadout, deployedRecord,
- mergeTarget, modeOffBonds, readyFundsPrompt, ownerBandId, ownDiyRecord,
+ mergeTarget, modeOffBonds, readyFundsPrompt, ownerBandId, ownDiyRecord, ownStandIn,
} from '../ui/gameLogic.js';
import { toast } from '../ui/toasts.js';
import { BriefingScreen } from './briefing.js';
@@ -240,6 +240,10 @@ function MatchScreen() {
// the own pieces' records: a DIY slot the player filled is its 自选 operator (0.2.0, m.private.diy — position, range,
// name; gameLogic/diy.js), every other chess its data record
const ownChess = (id) => { const c = gd.chess(id); return ownDiyRecord(c, live.current.priv, { chess: data.get('chess'), backups: data.get('backups') }) || c; };
+ // what the own pieces show (names and art of the facing wheel, the underframe, the equip-replace dialog): a chess the
+ // player does not own is its stand-in (0.2.0 补位 — the owner's recall of the official mode, 2026-10-06); rules keep
+ // ownChess (the composed record carries the chess's identity anyway)
+ const ownShown = (id) => { const c = ownChess(id); return ownStandIn(c, live.current.priv, data.get('backups')) || c; };
const placeCtx = useMemo(() => placementContext({
priv, stage: gd.stage(pub?.stageId), editable, field: deployField,
getChess: (id) => { const c = gd.chess(id); return ownDiyRecord(c, priv, { chess: data.get('chess'), backups: data.get('backups') }) || c; },
@@ -729,7 +733,7 @@ function MatchScreen() {
const L = live.current;
if (intent.confirmReplace) {
// both slots used: the player picks the equipped item to destroy (cancel ⇒ nothing is sent)
- const request = replaceRequest(L.placeCtx, intent, ownChess, gd.item);
+ const request = replaceRequest(L.placeCtx, intent, ownShown, gd.item);
if (request) {
const uid = await openReplaceRef.current(request);
if (!Number.isInteger(uid)) { audio.sfx('back', { volume: 0.5 }); return; }
@@ -755,7 +759,7 @@ function MatchScreen() {
/** Open the direction wheel for a legal board drop (the piece stays on the tile meanwhile). */
const openFacing = (entry, t) => {
const piece = entry.piece;
- const rec = piece.kind === 'item' ? gd.item(piece.id) : piece.kind === 'token' ? gd.token(piece.id) : ownChess(piece.id);
+ const rec = piece.kind === 'item' ? gd.item(piece.id) : piece.kind === 'token' ? gd.token(piece.id) : ownShown(piece.id);
holdPiece(view, piece.uid, { row: t.row, col: t.col });
setSel(null);
setFacing({ uid: piece.uid, piece, row: t.row, col: t.col, grid: previewGrid(lookups, piece), name: rec?.name || '' });
@@ -1279,7 +1283,7 @@ function MatchScreen() {
${bondPop ? html`<${BondPopup} bondId=${bondPop.bondId} entry=${bondPop.entry} priv=${bondPop.priv} banned=${pub?.bannedChess || []} owner=${bondPop.name}
off=${offBonds.has(bondPop.bondId)}
place=${bpPlace} over=${!!resolved && bpPlace === dSide}
- onClose=${() => setBondOpen(null)} onMember=${(id, items) => setDetail({ kind: 'chess', id, owner: bondPop.ownerId, items: items || null })} />` : null}
+ onClose=${() => setBondOpen(null)} onMember=${(id, items, standInFor) => setDetail({ kind: 'chess', id, owner: bondPop.ownerId, items: items || null, standInFor: standInFor || null })} />` : null}
${resolved ? html`<${DetailPanel} detail=${resolved} snapHp=${snapHp} onClose=${() => { setDetail(null); setSel(null); }}
bonds=${detailBonds} offBonds=${offBonds} loadout=${detailLoadout} side=${dSide} shopOpen=${shopOpen} live=${liveStats}
@@ -1287,7 +1291,7 @@ function MatchScreen() {
${selEntry && editable && !facing && !drag && showPrep ? html`<${Underframe} key=${sel.uid} view=${view} uid=${sel.uid}
row=${pieceTile(selEntry)?.row} col=${pieceTile(selEntry)?.col} actions=${underframeActions(placeCtx, sel.uid)} busy=${selBusy}
- name=${(selEntry.piece.kind === 'item' ? gd.item(selEntry.piece.id) : selEntry.piece.kind === 'token' ? gd.token(selEntry.piece.id) : ownChess(selEntry.piece.id))?.name || ''}
+ name=${(selEntry.piece.kind === 'item' ? gd.item(selEntry.piece.id) : selEntry.piece.kind === 'token' ? gd.token(selEntry.piece.id) : ownShown(selEntry.piece.id))?.name || ''}
onRetreat=${retreatSel} onSell=${sellSel} onDestroy=${sellSel} />` : null}
diff --git a/public/js/screens/game/standInTags.js b/public/js/screens/game/standInTags.js
index 7e49e264..3753655d 100644
--- a/public/js/screens/game/standInTags.js
+++ b/public/js/screens/game/standInTags.js
@@ -1,20 +1,21 @@
-// public/js/screens/game/standInTags.js — 0.2.0 补位: the 「替补:X」 tag under the player's own prep pieces (hand,
-// 临时整备区, board) of a chess they do not own (m.private.standIns). The hand keeps the chess's own model (the card shows
-// the original operator); on the board the model is the stand-in's (render/app.js pieceInfo) — the tag says which
-// chess it is either way. 0.2.0 自选编队: the 「自选」 tag under the player's own pieces of a DIY slot it filled
+// public/js/screens/game/standInTags.js — 0.2.0 补位: the small 「替补」 tag under the player's own prep pieces (hand,
+// 临时整备区, board) of a chess they do not own (m.private.standIns): the model is the stand-in's everywhere
+// (render/app.js pieceInfo — the owner's recall of the official mode, 2026-10-06), the tag says it fields for another
+// chess (the detail card names it). 0.2.0 自选编队: the 「自选」 tag under the player's own pieces of a DIY slot it filled
// (m.private.diy) — the model is the operator's everywhere. Anchored to each piece's drawn body (view.pieceScreenRect,
// followed every frame like the direction wheel follows its tile); never takes the pointer, so the pieces stay
// draggable.
import { useEffect, useRef, useState } from '../../../vendor/hooks.module.js';
import { html } from '../../ui/components.js';
-import { fieldsStandIn, standInOf, standInLabel, ownDiyRecord } from '../../ui/gameLogic.js';
+import { ownStandIn, standInLabel, standInTip, ownDiyRecord } from '../../ui/gameLogic.js';
import { t } from '../../../../shared/i18n.js';
const rawOf = (view) => (view && view.raw) || view || null;
/**
- * The player's own prep pieces that fight as stand-ins: [{ uid, label, name, area }] (hand, temp, board).
+ * The player's own prep pieces that fight as stand-ins: [{ uid, label, name, area, tip }] (hand, temp, board; `name` =
+ * the stand-in's, `tip` = which chess it fields for).
* @param {any} priv m.private @param {(id: string) => any} getChess @param {any} backups data/backups.json
*/
export function standInPieces(priv, getChess, backups) {
@@ -23,9 +24,8 @@ export function standInPieces(priv, getChess, backups) {
const add = (p, area) => {
if (!p || p.kind !== 'chess' || !Number.isInteger(p.uid)) return;
const c = getChess(p.id);
- if (!fieldsStandIn(priv, c)) return;
- const si = standInOf(c, backups);
- if (si) out.push({ uid: p.uid, label: standInLabel(si), name: si.name, area });
+ const si = ownStandIn(c, priv, backups);
+ if (si) out.push({ uid: p.uid, label: standInLabel(si), name: si.name, area, tip: standInTip(si, c.name) });
};
for (const p of Array.isArray(priv.hand) ? priv.hand : []) add(p, 'hand');
for (const p of Array.isArray(priv.temp) ? priv.temp : []) add(p, 'temp');
@@ -85,8 +85,8 @@ export function StandInTags({ view, priv, getChess, backups, diyData = null }) {
return html`
${items.map((it) => {
const p = pos[it.uid];
- return p ? html`${it.label}` : null;
})}
`;
diff --git a/public/js/screens/ownership.js b/public/js/screens/ownership.js
index bb6e4b42..4a997b95 100644
--- a/public/js/screens/ownership.js
+++ b/public/js/screens/ownership.js
@@ -1,8 +1,10 @@
// 干员持有 (operator ownership, 0.2.0 补位 — the approved plan, owner's decision 2026-10-05): the second tab of the
// 干员调配 overlay (screens/loadout.js). The 53 NORMAL chess of this season's shop, grouped by tier — each card shows the
// operator and its official stand-in (avatar, name, the skill it fields) and switches between 持有 and 未持有 with a
-// tap. A chess marked 未持有 keeps its shop / hand card (name, bonds, price, with a 「替补:X」 badge) and is deployed
-// as the stand-in (model, skills, talents, range). PRESET chess always field their own operator and are not listed.
+// tap. A chess marked 未持有 is shown as its stand-in in the match — shop and reward cards, hand, board, detail card,
+// result (portrait, name, model, skills, talents, range; a small 「替补」 mark — the owner's recall of the official mode,
+// 2026-10-06) — while its bonds, 特质, tier, price and merge stay the chess's. PRESET chess always field their own
+// operator and are not listed.
// Default: everything owned; 全部持有 resets. Out of match: the next match takes the list (in co-op it only affects the
// player's own pieces). The list lives in ui/loadoutSync.js (localStorage + room.ownership); the model is
// ui/ownershipModel.js. Styles: css/screens/loadout.css (own-*).
@@ -14,6 +16,7 @@ import { chessAvatarUrl } from '../ui/assetUrls.js';
import { data } from '../data.js';
import { PROF_NAME } from '../ui/loadoutModel.js';
import { ownershipRoster, rosterByTier, isOwned, standInSummary } from '../ui/ownershipModel.js';
+import { t } from '../../../shared/i18n.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V', 'VI'];
@@ -62,7 +65,7 @@ export function OwnershipPanel({ m, roster, notOwned, onToggle }) {
const groups = rosterByTier(list);
return html`
${bonds.map((b) => html`
diff --git a/public/js/ui/bondStrip.js b/public/js/ui/bondStrip.js
index d17a3b0d..8d557d68 100644
--- a/public/js/ui/bondStrip.js
+++ b/public/js/ui/bondStrip.js
@@ -18,10 +18,13 @@
// 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).
+// 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).
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, diyGetter, diyRecordFor } from './gameLogic.js';
+import { sortBonds, bondMembers, nextThreshold, bondTier, harmonyMembers, HARMONY_BOND, memberHeadCount, briefingBondTip, diyGetter, diyRecordFor, memberStandIn, standInForText } from './gameLogic.js';
import { formatBondEffect } from './richText.js';
import { bondIconUrl } from './assetUrls.js';
import { data } from '../data.js';
@@ -83,11 +86,12 @@ 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, items?:string[]|null)=>void,
+ * onMember?: (chessId:string, items?:string[]|null, standInFor?: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
+ * gets a 变形同构体 row's item ids too (its card shows the pair and the granted chip), null for a plain member, and a
+ * stand-in row's `standInFor` (0.2.0 补位: a teammate's card then shows the stand-in too); `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
@@ -105,13 +109,19 @@ export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember,
const getChess = diyGetter((id) => data.lookup('chess', id), priv, dd);
const pieceRecord = (p) => (p && p.diy && typeof p.diy === 'object' ? diyRecordFor(data.lookup('chess', p.id), p.diy, dd) : null) || getChess(p.id);
const members = bondMembers(b, priv, banned, getChess, (id) => data.lookup('items', id), pieceRecord);
+ // 补位: the stand-in a row shows (a 自选 row is its operator already)
+ const rawChess = (id) => data.lookup('chess', id);
+ const siOf = (mb) => (mb.diy ? null : memberStandIn(priv, rawChess(mb.id), data.get('backups'), rawChess));
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 harmonyBy = harmony ? harmonyMembers(priv, getChess).map((x) => {
+ const si = memberStandIn(priv, rawChess(x.id), data.get('backups'), rawChess);
+ return si ? { ...x, name: si.name } : x;
+ }) : [];
const harmonyName = data.lookup('bonds', HARMONY_BOND)?.name || '调和';
const harmonyText = `${harmonyBy.length ? `${harmonyBy.map((x) => x.name).join('、')} 在场` : `${harmonyName}已激活`}:核心盟约激活人数 +${harmony}`;
return html`
`;
diff --git a/public/js/ui/detailPanel.js b/public/js/ui/detailPanel.js
index 9115991e..74174911 100644
--- a/public/js/ui/detailPanel.js
+++ b/public/js/ui/detailPanel.js
@@ -33,7 +33,7 @@
import { html, Icon, TierChip, MicroLabel, Button, confirmDialog, useTicker } from './components.js';
import { Img, RichText, UnitThumb, BondGlyph, GIcon, diyToken } from './gameComponents.js';
-import { attackInterval, rangeGridBox, fmtNum, tileKey, chessLoadout, nextThreshold, bondTier, briefingBondTip, pieceBondIds, grantedBonds, morphPairings, fieldsStandIn, standInOf, standInLoadout, standInLabel, ownDiyRecord, ownDiyPick, diyRecordFor, pickGetter } from './gameLogic.js';
+import { attackInterval, rangeGridBox, fmtNum, tileKey, chessLoadout, nextThreshold, bondTier, briefingBondTip, pieceBondIds, grantedBonds, morphPairings, ownStandIn, standInOf, standInLoadout, standInLabel, standInTip, standInForText, ownDiyRecord, ownDiyPick, diyRecordFor, pickGetter } from './gameLogic.js';
import { chessPortraitUrl, skillIconUrl, skillRecordIconUrl, profIconUrl, subProfIconUrl, itemIconUrl, enemyIconUrl, tokenAvatarUrl, factionIconUrl, uiUrl, moduleTypeIconUrl } from './assetUrls.js';
import { abilityRows } from './abilityLines.js';
import { data } from '../data.js';
@@ -361,8 +361,10 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond
// 0.2.0 自选编队: `chess` is then the composed 自选 record (the operator, the slot's tier / price); its skill and module are
// the pick's — the lookups resolve the slot's ids to the same pick (shared/diy.js), so the loadout reads them as defaults
const getChess = diy ? pickGetter((id) => data.lookup('chess', id), diy, { chess: data.get('chess'), backups: data.get('backups') }) : (id) => data.lookup('chess', id);
- // 0.2.0 补位: a chess fielded as its stand-in keeps its identity (name, tier, bonds, 特质, sell price) and shows the
- // stand-in's body — portrait, class, 特性, stats, range, skill, talents, module (its backup selection, no loadout)
+ // 0.2.0 补位: a chess fielded as its stand-in shows the stand-in — portrait, name, class, 特性, stats, range, skill,
+ // talents, module (its backup selection, no loadout) — under a small 「替补」 tag, with the replaced operator's name
+ // where the English name usually is (「银灰的替补」, [ASSUMED] placement); the chess's tier, bonds, 特质 and sell price
+ // still apply (the owner's recall of the official mode, 2026-10-06)
const si = standIn && standIn.standInFor ? standIn : null;
const body = si || chess;
const lo = si ? standInLoadout(si, getChess, data.get('backups')) : chessLoadout(chess, loadout, getChess);
@@ -387,18 +389,18 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond
blocks.head = html`
${swapIcon ? html`` : html`<${GIcon} name="refresh" />`}
diff --git a/public/js/ui/fallbackField.js b/public/js/ui/fallbackField.js
index 690785f3..9fd13295 100644
--- a/public/js/ui/fallbackField.js
+++ b/public/js/ui/fallbackField.js
@@ -21,7 +21,7 @@ import { render } from '../../vendor/preact.module.js';
import { html, TierChip } from './components.js';
import { GEO } from '../../../shared/constants.js';
import { chessAvatarUrl, itemIconUrl, tokenAvatarUrl, enemyIconUrl } from './assetUrls.js';
-import { tileKey, hasFlag, UF, penPlacement, PEN, fieldTile, fieldsStandIn, standInOf, ownDiyRecord } from './gameLogic.js';
+import { tileKey, hasFlag, UF, penPlacement, PEN, fieldTile, ownStandIn, ownDiyRecord } from './gameLogic.js';
const DRAG_PX = 6;
const DMG_TTL = 900;
@@ -119,14 +119,16 @@ export function createFallbackView(host, opts = {}) {
const tempPos = (L, idx) => ({ x: (GEO.TEMP_C0 + idx) * L.tile, y: (L.rows + 1.72) * L.tile });
// ---- pieces (prep) ---------------------------------------------------------------------------------------
- function pieceArt(p, area = null) {
+ function pieceArt(p) {
const mm = m();
if (p.kind === 'item') return itemIconUrl(mm, lookup('items', p.id));
if (p.kind === 'token') return tokenAvatarUrl(mm, p.id);
const chess = lookup('chess', p.id);
- // 0.2.0 补位: on the board a chess the player does not own is its stand-in (render/app.js pieceInfo)
- const si = area === 'board' && fieldsStandIn(st.priv, chess) ? standInOf(chess, dataStore?.get?.('backups') ?? null) : null;
- return chessAvatarUrl(mm, si || ownDiy(chess) || chess);
+ return chessAvatarUrl(mm, ownSi(chess) || ownDiy(chess) || chess);
+ }
+ /** 0.2.0 补位: the player's own piece of a chess it does not own is its stand-in, bench and board alike (render/app.js pieceInfo) */
+ function ownSi(chess) {
+ return chess ? ownStandIn(chess, st.priv, dataStore?.get?.('backups') ?? null) : null;
}
/** 0.2.0 自选编队: the player's own piece of a DIY slot it filled is its operator (gameLogic ownDiyRecord) */
function ownDiy(chess) {
@@ -136,11 +138,11 @@ export function createFallbackView(host, opts = {}) {
if (p.kind === 'item') return lookup('items', p.id)?.name || '道具';
if (p.kind === 'token') return lookup('tokens', p.id)?.name || '召唤物';
const chess = lookup('chess', p.id);
- return (ownDiy(chess) || chess)?.name || '干员';
+ return (ownSi(chess) || ownDiy(chess) || chess)?.name || '干员';
}
function Piece({ p, x, y, L, area }) {
- const src = pieceArt(p, area);
+ const src = pieceArt(p);
const dragging = st.drag?.uid === p.uid && st.drag.moved;
const golden = !!p.golden;
const tier = p.tier || lookup('chess', p.id)?.tier || lookup('items', p.id)?.tier;
diff --git a/public/js/ui/gameComponents.js b/public/js/ui/gameComponents.js
index 43ad27e0..0552017c 100644
--- a/public/js/ui/gameComponents.js
+++ b/public/js/ui/gameComponents.js
@@ -5,6 +5,7 @@ import { useState, useMemo } from '../../vendor/hooks.module.js';
import { html, Icon, TierChip, Tooltip } from './components.js';
import { data, useData, localAsset } from '../data.js';
import { parseRichText, rtClassName } from './richText.js';
+import { t as tr } from '../../../shared/i18n.js';
import {
uiUrl, chessAvatarUrl, chessPortraitUrl, itemIconUrl, tokenAvatarUrl, enemyIconUrl, bondIconUrl, bandIconUrl,
} from './assetUrls.js';
@@ -101,15 +102,18 @@ export const isGoldenPiece = (piece, chess) => !!(piece?.golden || chess?.isGold
* Square unit thumbnail for a piece / chess / item / token / enemy: art + tier chip + elite frame.
* @param {{ kind?: 'chess'|'item'|'token'|'enemy', id: string, golden?: boolean, size?: 'xs'|'sm'|'md'|'lg', tier?: number,
* showTier?: boolean, class?: string, dim?: boolean, badge?: any, title?: string, rec?: any }} props
- * `rec`: the chess record to draw instead of the data's (a 自选 piece's composed record — 0.2.0, gameLogic/diy.js)
+ * `rec`: the chess record to draw instead of the data's (a 自选 piece's composed record — 0.2.0, gameLogic/diy.js; a
+ * 补位 stand-in's — gameLogic/standIn.js: drawn with a small 「替补」 mark in the corner)
*/
export function UnitThumb({ kind = 'chess', id, golden, size = 'md', tier, showTier = true, class: cls, dim = false, badge = null, title, rec = null }) {
const m = data.get('assets');
let src = null;
let name = '';
let t = tier;
+ let si = null;
if (kind === 'chess') {
const c = rec || data.lookup('chess', id);
+ si = rec && typeof rec.standInFor === 'string' && rec.standInFor ? rec : null;
src = chessAvatarUrl(m, c);
name = c?.name || '';
t = t ?? c?.tier;
@@ -137,6 +141,7 @@ export function UnitThumb({ kind = 'chess', id, golden, size = 'md', tier, showT
${showTier && t && kind !== 'enemy' && kind !== 'token' ? html`<${TierChip} tier=${t} golden=${golden} size="sm" class="uthumb__tier" />` : null}
${kind === 'token' ? html`召唤` : null}
+ ${si ? html`${tr('替补')}` : null}
${badge}
`;
}
diff --git a/public/js/ui/gameLogic.js b/public/js/ui/gameLogic.js
index 2b29e4be..2ed7a5bf 100644
--- a/public/js/ui/gameLogic.js
+++ b/public/js/ui/gameLogic.js
@@ -41,6 +41,6 @@ export { shortcutFor, closesOnFieldPress, shortcutBlocked } from './gameLogic/sh
export { DEFAULT_SETTINGS, sanitizeSettings } from './gameLogic/settings.js';
export { normalizeResult } from './gameLogic/result.js';
export { chessLoadout, unitLoadout } from './gameLogic/loadout.js';
-export { standInIds, fieldsStandIn, standInOf, cardStandIn, standInGetter, standInLoadout, deployedRecord, deployedModuleId, standInLabel } from './gameLogic/standIn.js';
+export { standInIds, fieldsStandIn, standInOf, ownStandIn, cardStandIn, memberStandIn, standInGetter, standInLoadout, deployedRecord, deployedModuleId, standInLabel, standInForText, standInTip } from './gameLogic/standIn.js';
export { diyPicks, ownDiyPick, diyRecordFor, ownDiyRecord, cardDiy, diyGetter, pickGetter, isDiyRecord } from './gameLogic/diy.js';
export { panelSide, panelSlots, PANEL_RIGHT_GAP, PANEL_RIGHT_BOTTOM, PANEL_RIGHT_BOTTOM_SHOP, BPOP, bondPopupPlace } from './gameLogic/panel.js';
diff --git a/public/js/ui/gameLogic/standIn.js b/public/js/ui/gameLogic/standIn.js
index 7bb3f8b7..61b06c66 100644
--- a/public/js/ui/gameLogic/standIn.js
+++ b/public/js/ui/gameLogic/standIn.js
@@ -1,18 +1,23 @@
// ui/gameLogic/standIn.js — 补位 stand-ins in the match UI (0.2.0, the approved plan — owner's decision 2026-10-05).
// Re-exported from ../gameLogic.js.
//
-// A chess the player marked as not owned (干员持有) keeps its card — name, bonds, price, the shop / reward / hand art —
-// with a 「替补:」 badge, and is deployed as its official stand-in: the board model, the battle unit and the
-// detail card's body (portrait, class, 特性, stats, range, skill, talents, module) are the stand-in's, its identity
-// (name, tier, bonds, 特质) the chess's. Whose pieces: the player's own come from m.private.standIns (the list the seat
-// had when the match started — the server's, not this browser's current setting, which applies from the next match);
-// another player's units say it themselves (UnitInfo `standInFor`: the sim's, and Match.prepFieldMeta's for scouting).
-// The composed record is shared/standIn.js standInRecord over data/backups.json — the one the sim fields.
+// A chess the player marked as not owned (干员持有) is SHOWN as its official stand-in wherever the player's copy of it
+// appears — the shop and reward cards, the hand (整备区) and 临时整备区 pieces, the board, the detail card, the result
+// screen, a teammate's prep scouting and bond popups — the owner's first-hand recall of the official mode, 2026-10-06
+// (it reverses the first 0.2.0 build, where cards and the bench kept the chess's art and name with a 「替补:X」 badge).
+// Only what is shown changes: the record (shared/standIn.js standInRecord over data/backups.json — the one the sim
+// fields) keeps the chess's identity — ids, tier, bonds, 特质, price, merge — so every rule still reads the chess; its
+// body — portrait / avatar, name, class, model, 特性, stats, range, skill, talents, module — is the stand-in's; a small
+// 「替补」 mark (standInLabel) and the replaced operator's name on the detail card (standInForText) tell which chess it
+// is. Whose pieces: the player's own come from m.private.standIns (the list the seat had when the match started — the
+// server's, not this browser's current setting, which applies from the next match); another player's units say it
+// themselves (UnitInfo `standInFor`: the sim's, Match.prepFieldMeta's for scouting, the m.result lineups).
import { standInRecord } from '../../../../shared/standIn.js';
import { resolveLoadout } from '../../../../shared/protocol.js';
import { chessLoadout } from './loadout.js';
import { isObj } from './shared.js';
+import { t } from '../../../../shared/i18n.js';
/** Base chess ids the player fields as stand-ins in this match (m.private.standIns; [] when absent). */
export function standInIds(priv) {
@@ -33,10 +38,13 @@ const CACHE = new WeakMap();
/**
* The composed 补位 record of a chess record (shared/standIn.js standInRecord: the chess's identity, the stand-in's
* body; `standInFor` = the replaced charId), cached per data object; null for a PRESET / 自选 chess or missing data.
+ * A composed record is its own stand-in (a lookup that already hands out stand-in records stays idempotent).
* @param {any} chess data/chess.json record @param {any} backups data/backups.json
*/
export function standInOf(chess, backups) {
- if (!isObj(chess) || !isObj(backups)) return null;
+ if (!isObj(chess)) return null;
+ if (typeof chess.standInFor === 'string' && chess.standInFor) return chess;
+ if (!isObj(backups)) return null;
let m = CACHE.get(backups);
if (!m) { m = new WeakMap(); CACHE.set(backups, m); }
if (m.has(chess)) return m.get(chess);
@@ -47,8 +55,18 @@ export function standInOf(chess, backups) {
}
/**
- * The stand-in record the card of a chess shows for this viewer, or null: the player's own piece / card (m.private
- * standIns), or a unit that carries `standInFor` (another player's — the sim's UnitInfo, prep scouting).
+ * The stand-in record the player's own card / piece of `chess` shows (the player fields its stand-in: m.private
+ * standIns), or null.
+ * @param {any} chess @param {any} priv m.private @param {any} backups
+ */
+export function ownStandIn(chess, priv, backups) {
+ return fieldsStandIn(priv, chess) ? standInOf(chess, backups) : null;
+}
+
+/**
+ * The stand-in record the card of a chess shows for this viewer, or null: a unit / lineup entry that carries
+ * `standInFor` (another player's — the sim's UnitInfo, prep scouting, m.result), else the player's own piece / card
+ * (m.private standIns).
* @param {any} chess @param {{ priv?: any, unit?: any, backups?: any }} o
*/
export function cardStandIn(chess, { priv = null, unit = null, backups = null } = {}) {
@@ -57,6 +75,27 @@ export function cardStandIn(chess, { priv = null, unit = null, backups = null }
return mine ? standInOf(chess, backups) : null;
}
+/**
+ * The stand-in a bond popup's member row of base chess `chess` shows for the board `priv`, or null: a chess the player
+ * fields as its stand-in (m.private.standIns — any copy, in play or not, as the shop shows it), or one whose piece on a
+ * teammate's board says it is a stand-in (ui/watchBonds.js ownerBoard: the units' `standInFor`).
+ * @param {any} priv m.private, or a teammate's board ({ board, hand, temp } of pieces with `standInFor`)
+ * @param {any} chess the member's chess record @param {any} backups
+ * @param {(id: string) => any} [getChess] the pieces' records (their base ids)
+ */
+export function memberStandIn(priv, chess, backups, getChess = () => null) {
+ if (!isObj(chess) || typeof chess.chessId !== 'string') return null;
+ if (fieldsStandIn(priv, chess)) return standInOf(chess, backups);
+ const base = chess.baseId || chess.chessId;
+ const baseOf = (id) => getChess(id)?.baseId || (typeof id === 'string' ? id.replace(/_b$/, '_a') : id);
+ for (const list of [priv?.board, priv?.hand, priv?.temp]) {
+ for (const p of Array.isArray(list) ? list : []) {
+ if (isObj(p) && typeof p.standInFor === 'string' && p.standInFor && baseOf(p.id) === base) return standInOf(chess, backups);
+ }
+ }
+ return null;
+}
+
/** id → stand-in record (else the chess record): the lookup a stand-in's loadout / options resolve against. */
export function standInGetter(getChess, backups) {
return (id) => {
@@ -104,7 +143,17 @@ export function deployedModuleId(chess, priv, getChess, backups) {
} catch { return null; }
}
-/** The 「替补:X」 badge text of a stand-in record (null without one). */
+/** The small 「替补」 mark of a stand-in record (null without one): cards, the own pieces' tags, thumbnails. */
export function standInLabel(rec) {
- return isObj(rec) && rec.standInFor && rec.name ? `替补:${rec.name}` : null;
+ return isObj(rec) && rec.standInFor ? t('替补') : null;
+}
+
+/** The replaced operator's line of a stand-in's detail card: 「银灰的替补」 (`chessName` = the chess's own name). */
+export function standInForText(chessName) {
+ return t('{name}的替补', { name: chessName || '' });
+}
+
+/** The 「替补」 mark's title: which chess the stand-in fields for, and what stays the chess's ('' without a stand-in). */
+export function standInTip(rec, chessName) {
+ return isObj(rec) && rec.standInFor ? t('未持有{name}:由替补干员 {standIn} 上场(盟约、特质、阶级与价格不变)', { name: chessName || '', standIn: rec.name || '' }) : '';
}
diff --git a/public/js/ui/ownershipModel.js b/public/js/ui/ownershipModel.js
index 9b31fc76..41ba8b83 100644
--- a/public/js/ui/ownershipModel.js
+++ b/public/js/ui/ownershipModel.js
@@ -2,8 +2,9 @@
// shared with the sync (ui/loadoutSync.js) and the tests. The approved plan (owner's decision 2026-10-05): a player marks
// operators as not owned ("下掉"); only the 55 NORMAL chess can be dropped (shared/standIn.js isDroppableChess — the 74
// PRESET chess always field their own operator); default = everything owned; out of match (the next match takes it);
-// co-op: the player's own pieces only; no friend borrowing. A dropped chess keeps its card (name, bonds, price) and is
-// deployed as its official stand-in (data/backups.json).
+// co-op: the player's own pieces only; no friend borrowing. A dropped chess is deployed and shown as its official
+// stand-in (data/backups.json; the owner's recall of 2026-10-06: cards, pieces and the result show the stand-in) while
+// its bonds, 特质, tier, price and merge stay the chess's.
//
// The per-browser setting is a sorted list of base chess ids, persisted in localStorage (`sp.pref.ownership` =
// { v: 1, notOwned }) and sent with C2S `room.ownership { notOwned }` (the server keeps the droppable ids and drops the
diff --git a/public/js/ui/shopBar.js b/public/js/ui/shopBar.js
index b60fd627..67c48f73 100644
--- a/public/js/ui/shopBar.js
+++ b/public/js/ui/shopBar.js
@@ -18,15 +18,16 @@
// Every operator card shows the skill it will fight with — the player's 干员调配 loadout (m.private.loadout, DESIGN
// §16): the skill icon above the name, mint-framed with 已调配 in its title when it is not the default skill (and the
// module type of an elite card, with its official type icon when the local-client art has it). A chess the player does
-// not own (0.2.0 补位, m.private.standIns) keeps its card — name, portrait, bonds, price — with an ice 「替补:X」 badge,
-// and the skill icon is the stand-in's (its backup skill: the one that fights). A 自选 piece (0.2.0, m.private.diy — a DIY
-// slot the player filled, only ever in its own shop) shows the operator — name, portrait, class, the bonds of its
-// factions, the pick's skill and module — with a mint 「自选」 badge.
+// not own (0.2.0 补位, m.private.standIns) shows its stand-in — portrait, name, class and its backup skill, the one that
+// fights — with the chess's bonds, tier and price and a small ice 「替补」 mark (the owner's recall of the official mode,
+// 2026-10-06; the replaced operator's name is in the mark's title and on the detail card the first tap opens). A 自选
+// piece (0.2.0, m.private.diy — a DIY slot the player filled, only ever in its own shop) shows the operator — name,
+// portrait, class, the bonds of its factions, the pick's skill and module — with a mint 「自选」 badge.
import { useEffect, useState } from '../../vendor/hooks.module.js';
import { html, Icon, HexBadge, TierChip, Tooltip, MicroLabel } from './components.js';
import { Img, BondGlyph, CoinGlyph, GIcon, RichText } from './gameComponents.js';
-import { priceTone, mergeProgress, mergeTarget, shopBlockReason, chessLoadout, offerHeader, briefingBondTip, fieldsStandIn, standInOf, standInLoadout, standInLabel, ownDiyRecord, diyGetter } from './gameLogic.js';
+import { priceTone, mergeProgress, mergeTarget, shopBlockReason, chessLoadout, offerHeader, briefingBondTip, ownStandIn, standInLoadout, standInLabel, standInTip, standInForText, ownDiyRecord, diyGetter } from './gameLogic.js';
import { chessPortraitUrl, itemIconUrl, profIconUrl, uiUrl, skillIconUrl, skillRecordIconUrl, moduleTypeIconUrl } from './assetUrls.js';
import { data } from '../data.js';
import { t } from '../../../shared/i18n.js';
@@ -71,7 +72,10 @@ export function ChessCard({ slot, idx, priv, frozen = false, reason = null, free
// 0.2.0 自选编队: a DIY slot the player filled is its operator (shared/diy.js, the pick of m.private.diy)
const diyData = { chess: data.get('chess'), backups: data.get('backups') };
const dr = c0 ? ownDiyRecord(c0, priv, diyData) : null;
- const c = dr || c0;
+ // 0.2.0 补位: the player's not-owned chess shows its stand-in (art, name, class, its backup skill); the composed record
+ // keeps the chess's identity — tier, bonds, price (a DIY slot is never droppable: one or the other)
+ const si = c0 && !dr ? ownStandIn(c0, priv, data.get('backups')) : null;
+ const c = dr || si || c0;
const m = data.get('assets');
const tier = c?.tier ?? 1;
const prog = mergeProgress(priv, slot.id, (id) => data.lookup('chess', id));
@@ -79,13 +83,12 @@ export function ChessCard({ slot, idx, priv, frozen = false, reason = null, free
const willMerge = !!hint;
const bonds = Array.isArray(c?.bonds) ? c.bonds : [];
const disabled = !!reason;
- // 0.2.0 补位: the player's not-owned chess is deployed as its stand-in — badge + the stand-in's skill
- const si = c && fieldsStandIn(priv, c) ? standInOf(c, data.get('backups')) : null;
const lo = si ? standInLoadout(si, LOOKUPS.getChess, data.get('backups')) : c ? chessLoadout(c, priv?.loadout, dr ? diyGetter(LOOKUPS.getChess, priv, diyData) : LOOKUPS.getChess) : null;
const tap = () => { if (onTap) onTap(idx); else if (!disabled) onBuy(idx); else onDetail(slot.id, 'chess', hint); };
- const card = html`