Files
ark-stronghold-protocol/docs/design/match.md
T
IceCodeNew e63214571d feedback7 dober: the bots plan a zero-range heavy guard (range 0-1: only its own tile) on a free enemy-road tile first (PR #339 by @IceCodeNew, that part only); golden: 9 of 18 matches move, no result flips
Seen: 蛇屠箱 and the seven other operators with the range 0-1 (角峰, 古米, 泡泡, 折桠, 菲莱, 塞雷娅, 余 — 16 chess records, all
melee blockers with block 3, the elite 角峰 4) block and hit only on the tile they stand on. The layout planner maximizes an exposure model whose
kill estimate saturates in a full layout; the tile then falls to a 1e-6 rng tie-break, so a guard stood off the road, where
it neither blocks nor hits (reproduced: planLayout with six of them on act2autochess_m04, seed 11 — 蛇屠箱 off the road; over
40 seeds of the eight stages 16 of 320 guards stood off the road: 11 because the eight guards outnumber the 7 free road tiles of
act1autochess_m03 / act2autochess_m02, 5 avoidably — after the change only the 11 are left).
Official: excel range_table "0-1" is the single grid (0,0).
Now: planLayoutSteps keeps the best road tile (model.ground) for a blocker whose deployed range grid (rangeRec: its
loadout's) is exactly one (0,0) grid, and takes it before the overall best; with no free road tile the best legal tile as
before (a high-ground 「可以放置于远程位」 blocker keeps its own preference first).
Not taken from the PR: the scarce-card penalty of buyScore, the item scoring (人事部文档 "only with nine 炎" contradicts the
official 部署上限变为 9), the 神秘 morph fit and the draft yield — AI heuristics with no official text, and the golden
swing they caused (a solo match that cleared the Hidden Core no longer did) came from them, not from the placement.
Golden (matches only; every other family unchanged, --twice identical): solo-FUNNY-1 (from R7: boss pool left 74740 -> 71550,
still a loss in R8), solo-NORMAL-1 (from R4: same win, Hidden Core cleared, team LP 26), solo-HARD-1 (from R4: boss pool left
3046117 -> 2940542), solo-ABYSS-1 (from R4: 2303334 -> 2281356, still a loss in R13), coop2-NORMAL-3 (from R4), coop2-HARD-4
(from R4), coop4-FUNNY-6 (from R3: virtualMs 128500 -> 127000), coop2-NORMAL-8-serverrun (from R3), coop2-NORMAL-70-diy (from
R6): rows, stats, lineups and rng draws differ, wins, losses and rounds passed do not.
botbench A/B on 12 seeds each (solo HARD / ABYSS, coop4 NORMAL / HARD): wins 1 / 0 / 11 / 3 both before and after, rounds
passed 13.00 / 11.92 / 14.00 / 13.00 -> 12.92 / 11.92 / 14.00 / 13.17, LP left 18.7 / 6.5 / 80.1 / 27.9 -> 18.8 / 7.4 / 83.7 / 28.0.
Tests: bot.test.js — the PR's saturated-layout case (蛇屠箱 blocks and hits only on a road tile, a real battle on both tiles;
the road full: a legal fallback) and every stage over 40 seeds (the eight guards fill min(8, free road tiles) road tiles);
both fail on the parent.
2026-10-08 20:58:20 +08:00

25 KiB
Raw Blame History

DESIGN §6, §16 — The match and meta engine (server/match), operator loadouts

Part of DESIGN.md (the index; section numbers are global).

6. Match & meta (server/match)

6.1 State machine (Match.js)

LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed (§18.2); all ready ⇒ skip; the operator loadout (§16) locks when it ends) → BAND_DRAFT (co-op: random order, ONE countdown — `BAND_TURN_SECONDS` 30 s per turn = m.public.deadline, AI seats pick at once, 1 skip each, a strategy a teammate already took is refused (队友已选), timeout ⇒ the strategy the player highlights (`g.bandFocus`) while free, else band_bldsk, else the first free one; solo: free pick, no timer, no skip; a single human: untimed) → BATTLE_CHECK (3 s)
→ loop r = 1..lastRound:
     ROUND_START   (income, upgrade price −1 (floor 0), temp NOT wiped — what overflowed after the last prep's deadline is shown in this
                    prep (§6.2 temp overflow), unfrozen shop slots rerolled, frozen kept, <进入休整期时> effects)
     [SP_DRAFT]    (if r ∈ spRounds: 机变 draft; co-op 30 s first picker / 16 s others; solo 3 cards; solo / single human untimed;
                    a card is picked with two taps, §18.2)
     PREP          (co-op: timer from config, ends when all alive humans+bots ready; solo / single human: untimed, ends on ready)
     PREP_END      (<休整期结束时> effects; unfrozen shop cleared; Ready is blocked while a personal choice or temp overflow is pending;
                    at the deadline personal choices are auto-picked first, then
                    the temp pieces due at this prep are resolved (chess sold back, items destroyed, summon stacks removed — they come back at the next round start, §20.1) — pieces that arrived
                    after Ready or during the <休整期结束时> effects wait for the next prep (§6.2 tempDue))
     COMBAT        (one Battle per alive player, all in parallel, 2×; watchers get snapshots; solo: pausable, §14 Solo pause)
     UNITE         (co-op only, if ≥1 leaked & ≥1 perfect: ≤2 perfect helpers in `unite.js helperOrder` order (research 08 §5 / PRTS 帮助:
                    most units on the field > an active bond > most standing units > seat; the pair ordered by units > active bond >
                    Σ active layers > standing > seat, the first one on the right-hand field) fight the union of leaks; no layer gains — with
                    the layers their own combat reached (the round's pending gains, PRTS "以其阵地当前的状态" [ASSUMED]);
                    a helper's operator knocked out at the end of its own combat enters down — carryState { down: true },
                    PRTS "上一阶段为退场状态的干员强制退场", its full redeploy timer — confirmed by the user (§19.3))
     SETTLE        (LP −min(leaks,10) per player (unite survivors to their source); eliminations; layer gains applied; coins; broadcast;
                    after a 联防 the outcome and every player's charge ride the SETTLE m.public as `uniteResult`, GitHub #235)
   R14 FINAL_ASSAULT (merged team LP; pairs by seat; shared boss pool; 120 real s countdown, no hard stop; overtime −1 team LP per
                    real s after 150 real s (§4, m.public.overtimeAt); no layer gains; team LP 0 ⇒ defeat at once (PRTS 直接失败),
                    pool 0 ⇒ victory — the first end the server registers decides, §20.5)
   R15 HIDDEN_CORE  (if difficulty ≥ NORMAL and Σ activated layers > 350 solo / 1200 co-op and team LP > 1)
→ RESULT (per-player stats, titles, rounds passed, victory/defeat)

Solo FUNNY has 9 rounds (boss at R9); everything is read from data/config.json → modes[modeId].

6.2 Player state (PlayerState.js) — authoritative per player

{ playerId, seat, name, isBot, connected,
  alive, lp, bandId,
  funds, shop: { level, upgradePrice, slots: [ Slot ], frozen: bool, freeRefreshes: n, rewardOffer: null | { tier, source:'merge'|'special', label, queued, slots:[Slot] } },  // a special offer (凯瑟琳's 定向投放 …) by its source's label; queued = offers behind the shown one (§21.2)
  hand: [ Piece|null ×10 ], temp: [ Piece|null ×5 ],
  board: Map<'r,c', Piece>,                          // chess & token pieces on the normal field
  bonds: { [bondId]: { count, active, tier, layers } },  // layers persist all match
  effects: [ EffectRef ],                             // band/choice/team/garrison persistent effects & counters
  personalChoice: null | { id, round, sourceItemId, cards: [ BountyCard ] }, // private PREP offer; raw cards on the server
  counters: { refreshes, buys, sells, … },
  stats: { … for results } }
Slot  = { kind:'chess'|'item', id, price, sold:false, frozen }
Piece = { uid, kind:'chess'|'item'|'token', id /* chessId (golden id if elite) | itemId | tokenId */,
          items:[itemPiece...] (chess only, max 2), count (token stacks), ownerUid (tokens), boughtRound,
          meta: { round?: { r, n: { key: count } } } }   // per-piece counters of the current round, module-prefixed keys (拉普兰德's first refresh, §21.1)

Intent handlers (validate → mutate → recompute bonds → push private view): buy(slot), refresh(), freeze(), levelUp(), sell(uid), move(uid, to), equip(itemUid, targetUid, replaceUid?), useArt(itemUid, row, col), destroy(uid), pickReward(idx), pickChoice(idx), setReady(bool). Every handler rejects when the phase forbids it (only PREP allows board/shop mutations; SP_DRAFT allows pickChoice only; reward pick allowed in PREP). Invalid intents return error with a code from shared/constants.js → ERR — the server never throws on bad input.

Summons (§20.1): an operator on the board sends its manually deployable summons to the hand — deploy-limit copies in one stack, no deploy slot (tokens.json placeable, gamedata.placeableTokens); moving or swapping it sends its placed summons back to their stack, withdrawing or selling it removes them. A range-bound summon (tokens.json ownerRange: 狼群, 流形) goes only on a tile of its owner's attack range (PlayerState._legal / summonRange); in a swap each piece is checked from the tile it ends on; an in-place re-orientation or promotion that leaves it outside sends it back to its stack with a toast (one still inside stays [ASSUMED], user-confirmed 2026-10-03), and a re-orientation whose lifted summon would have no room is refused with HAND_FULL; on the other paths it leaves the board and returns at the next round start (§21.3).

Equipment (research 04 §2 + addendum, 09 §1.2): 2 slots per chess; a third item on a full carrier replaces the equipped item replaceUid names (the replace dialog's pick, official UseEquipUp.unloadInstId; absent ⇒ the oldest; a uid not equipped on the target ⇒ BAD_TARGET, nothing changes) and the replaced item is destroyed. A consume-on-equip item (盟约之币, 博士投影 …) on a full carrier replaces the same way before it resolves, so the carrier is left with a free slot (the normal 博士投影 takes it until the next round start); a target its effect refuses (博士投影 on an elite) keeps both items — PRTS 卫戍协议/帮助 "达到上限强行佩戴会改为替换装备" names no exception (GitHub #263; the client opens the replace dialog for it, equipReplaces). Equipped items are otherwise locked: destroy (销毁) accepts hand / temp items only (BAD_TARGET 'equipped items are locked'); equipment goes back to the hand only when its carrier merges, is sold, or is destroyed by 突变细胞's transformation (the cell itself returns too; PRTS 帮助 "在失去该干员(干员出售、销毁、合并等)…时自动卸除", §21.1) — a sale is refused with HAND_FULL when there is no room for it; returned equipment is stowed before the operator a transformation gains and before a merge elite's summon cards, and every path that hands the player an item — returned equipment included — ends with the auto-merge, so no player holds two identical mergeable normal items (§21.7).

Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income min(3+r,12); prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; shared pool copies 12/14/18/16/8/5 (缪尔赛思 4) across all players; elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite onto the consumed deployed copy's tile, else to the hand (§20.11) + reward offer of 3 different free chess of tier min(level+1, 6) (copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); full hand blocks every buy / reward pick, a merge-completing one too (PRTS 卫戍协议/帮助 §手牌区, GitHub #82); overflow to temp; a free hand slot pulls the temp pieces back in at once, right → left (PlayerState._fillHandFromTemp); temp blocks ready).

Temp overflow (临时整备区, row 8; user playtest #3 item 3). Passive gains that find the hand full (merge elites, grants — the operator a 突变细胞 transformation gains included, §21.1 —, battle-result rewards, equipment returned by a merged / sold / transformed carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the first prep in which its player could act on it (PlayerState.tempDue(piece) vs prepsEnded; every write into a temp slot goes through _putTemp, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the <休整期结束时> effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the next prep. Un-readying (setReady(false)) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). server/match/audit.js (tools/matchrun.mjs --check) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).

Pointing Stick creates at most one pending offer per alive, non-Ready player in PREP. The owner confirms it with g.choice { idx, choiceId }; confirmation validates its ID, round and index, adds one bounty and clears the offer. Ready requires no pending offer as well as an empty temp area. The co-op prep deadline resolves the offer randomly before resolving temp and setting Ready; untimed solo / single-human prep does not schedule a separate choice timer. The offer is recipient-private, including after reconnect, and never enters m.public.sp or a teammate's scout view.

6.3 Bonds & layers (bondsMeta.js)

  • count = number of distinct base chess on the board carrying the bond (BOARD); BOARD_AND_DECK bonds also count the hand; 绝技 counts elites on board; 调和 adds +1 to each core bond count that has a real member on the board (the views mark such an entry harmony: 1, §21.26), etc. (research 02).
  • 变形同构体: an operator wearing it together with an item that has a giveBondId is a member of that bond — counted like any member (BOARD: on the board only; once per operator), in the battle, and in the client's member list (tagged 同构) and bond chips (§21.11).
  • tier from thresholds; active = tier ≥ 1.
  • layers[bondId] persist all match; gains come from garrisons/bands/items/choices/bond effects; IN_BATTLE gains are collected from BattleResult.layerGains at SETTLE (disabled in unite/boss/hidden).
  • A bond holds at most BOND_LAYER_CAP = 999 layers (the client's MAX_GARRISON_STACK, §20.12): every writer — PlayerState.addLayers, the SETTLE merge, Battle.addLayers, the dev tools — adds layerGainRoom(before, n) = min(n, 999 − before); a gain at the cap adds 0 and dispatches nothing (no onLayers, no layerGain hook, no 'layer' event), so the per-N milestones stop with the count.
  • Σ activated layers (hidden-core check) = Σ layers over bonds that are active at the moment of the check (end of R14 prep, per research).

6.4 Prep-phase effect dispatcher (effectsMeta.js)

A registry mapping effect keys → handler objects { onRoundStart, onPrepEnd, onGain(piece), onSold(piece), onRefresh, onPrice(slot), onBuy, onMerge, onLevelUp, onBattleResult }. Sources: garrison SERVER_* keys (per chess on board or hand as the key requires), band effects, item meta effects, bond meta effects (助力, 远见, 奇迹, 投资人…), choice/team cards. Content owners register handlers; the dispatcher owns ordering and the 投资人 "获得时 ×2/×3" multiplier. onBuy = a shop purchase (g.buy) only: a free pick or a grant never counts as 购买 (休露丝, 玛恩纳, 升华, 整备; §21.21). Equipped items run from a snapshot — the owned chess and each holder's items as that step begins (every [holder, item] pair, taken before the first item runs) — each only while it is still equipped on its still-owned holder (handlers move and destroy pieces: 突变细胞, 博士投影; §21.25).

6.5 Waves (waves.js)

Per match: pick stage (weighted among the mode's config.modes[m].stages, weight 50 each: 标准 FUNNY = 战场#01 act1autochess_m01 only, 险境 NORMAL = 8 stages, 绝境 / 终极 = 7 without 战场#01 — waves.js setupMatchWaves, test/match/stagepick.test.js), 3 of 6 typed enemy factions, disabled bond set (3 core + 4 add-on for NORMAL+, per research), boss (weighted), hidden boss. Per round: wave template (by mode & round) → replace placeholders per research 05 §3.2 algorithm → apply stat multipliers (research 00-INDEX §5 atk ×base×1.1^k, hp ×base×1.2^k, ABYSS speed ×1.15 from R3) → SpawnSpec[] + routes resolved on the stage. (Superseded by the official generator — §14 "Corrections from research 08/09" and the waves.js header.) A player's normal battle (and the bot rehearsal) runs withBounties(gd, round, wave, bounties, playerId): the bounty units inserted among their host action's own units, which are re-timed around them (client order); previewOf keeps one entry per action (wave.spawns + bountySpawns).

6.6 AI players (bot.js)

Pending Pointing Stick offers use the existing bounty scorer at bot prep entry, immediately after a successful cast, and before Ready. The timer / disconnected-seat finalizer randomly resolves any remaining offer before temp and Ready.

Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP; never one built around a bond the mode switches off — gd.bandBondIds, §21.26); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a blocker whose range is its own tile only — range 0-1 — takes a free tile of the enemy road first; a tactician's 援军 within PlayerState.summonRange; a melee chess whose trait reads 「可以放置于远程位」 (钩索师 / 推击手) on a 高台 whose range covers the enemy road, else on the ground, counted as a blocker in the lineup either way — §3; g.move judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too; the buy loop's bench shed never sells a piece gained since the bot's last prep, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (botPrepBeginSteps, 25–70 ms late in a 4-bot match), the layout rehearsal and botPrepEndSteps — runs in wall-clock slices of botSliceMs (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time). Under AI 托管 a human's own 自选 pieces score DIY_PIECE_BONUS (12) more in the shop / reward choice and the 调度中心 is levelled one round earlier for the step that opens a slotted tier (levelUp, diyOpensAt; 0.2.0 — bots field no 自选 piece).


16. Operator loadout: skill & module selection (v2.1, user playtest #1)

Official rule (bwiki 盟约, 更新公告 5114): before a match the player cannot change an operator's tier, but can change the equipped skill and module of each chess; some non-default choices are stronger. Scope in the remake (no accounts ⇒ everything the chess's status unlocks is selectable):

  • Skills: any skill unlocked at the chess's status (evolvePhase: E1 ⇒ S1–S2, E2 ⇒ S1–S3) at the chess's skill level (normal Lv4, elite Lv7). Default = defaultSkillIndex.
  • Modules: only matter for the elite (golden) chess (equipLevel 1, or 3 at T6): any module of that character (or none); default = defaultUniEquipId. Normal chess have no module (equipLevel 0).

Data (data/chess.json, built by tools/build-data.mjs): every chess keeps skill (the default, unchanged shape) and gains skills: [SkillRecord…] (index, skillId, iconId, name, desc/descRaw, skillType, durationType, duration, spType, spCost, initSp, maxChargeTime, bb (at the chess's skill level), bbStr, rangeGrid, trigger {rule, customRangeGrid} resolved for THAT skill index, isDefault). Golden chess gain modules: [ModuleRecord…] = { uniEquipId, name, typeName, icon, isDefault, attr: {maxHp, atk, def, res, aspd, cost, blockCnt, respawnTime…} (flat adds at the chess's equipLevel), traitOverride: {desc, descRaw, bb} | null, talentChanges: [{talentIndex, name, desc, bb}] } plus statsBase (stats WITHOUT any module); stats stays = stats with the default module (back-compat).

Loadout wire & state: the client keeps a per-browser loadout { [baseChessId]: { skill: index, module: uniEquipId | 'none' } } (localStorage) and sends it with C2S room.loadout { entries } (validated: known visible chess, legal skill index for that chess's normal AND elite status, legal module id). The lobby stores it on the seat; at match start Match receives seats[].loadout; PlayerState keeps it immutable for the match; board pieces are resolved to {chessId, skillIndex, moduleId} when building PlayerBattleInput.units[] (fields skillIndex, moduleId; BattleSpec carries them). Bots use defaults. m.private exposes the effective loadout so the UI (shop cards, detail panel) shows the chosen skill/module.

Sim: simdata resolves a unit def for (chessId, skillIndex, moduleId) (cached): skill = the selected SkillRecord; stats = statsBase + module attr (elite) ; trait/talents = base with the selected module's overrides. Kit contract (backward compatible): a kit function (bb, chess) => { skill?, skills?: { [skillId]: SkillSpec }, talents, trait } receives chess.skill = the selected skill and bb = its blackboard; the loader uses skills[selectedSkillId] if present, else skill only when the selected skill is the chess's default, else the generic skill spec — talents/trait from the kit always apply. Every visible chess must get hand-authored specs for all of its selectable skills (tracked by a coverage test).

UI: a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; a 局内数值 section between the skills and the modules (普通 / 精锐 toggle, 精锐 first) draws the detail card's own stats block, 特性 and 天赋 for the chosen skill / module (§22.16); the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows its owner's skill/module from its UnitInfo skillIndex / moduleId, gameLogic.js unitLoadout) — which also shows the stats, 特性, talents and attack range the unit fights with (shared/loadoutRecord.js loadoutRecord / attackRangeGrid, the same composition the sim's getChess(id, loadout) uses; the deploy wheel's range preview too) — the range it is deployed with: a selected passive range skill (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1) included, over all 1284 loadouts equal to the battle's range at deployment; in battle and prep the card draws the live range (§21.16). In battle the renderer plays the equipped skill's own Spine clip and ON_SKILL_START sound (UnitInfo.skillIndex; data/assets.json lists icons, clips and sounds for every skill index).

导出 / 导入 a loadout: 导出 hands the current loadout out as a versioned envelope { kind: 'stronghold.loadout', v, exportedAt, count, entries } — copied through ui/clipboard.js copyText or downloaded as a file. 导入 accepts that envelope, the stored { v, entries } form and a bare { [chessId]: { skill, module } } map (serialised text too), and refuses another tool's kind, junk, an empty result and a newer v (parseImport — a newer payload is never mis-read). Both go through loadoutSync.applyLoadoutEntries → sanitizeEntries, so a preset from another build never sends the server an entry it would refuse, an import that keeps nothing changes nothing, and the result is reported as N imported / M not (ui/loadoutModel.js, screens/loadout.js). 干员持有 (0.2.0 补位): the overlay's second tab marks operators as not owned (the 55 NORMAL chess; per-browser sp.pref.ownership = { v, notOwned }, synced as room.ownership { notOwned } by the same sync engine as the loadout, ui/loadoutSync.js installPrefSync; 导出 / 导入 as { kind: 'stronghold.ownership', v, exportedAt, count, notOwned }). A not-owned chess fights as its official stand-in with the backup skill / module — the loadout never applies to it ("对于补位干员其技能不可更改") — and is shown as the stand-in wherever the player's copy appears (shop / reward cards, hand and 临时整备区, board, detail card, result lineup, a teammate's scouting and bond popups: portrait, name, model, with 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; the 0.2.0 补位 design subsections have the rules and sources. 自选编队 (0.2.0): the overlay's third tab fills the four DIY slots (5阶 ×2, 6阶 ×2) with an owned 6★ outside the chess pool (any of its three skills, any module of its elite form but a 集成战略 one) or a prototype (its locked skill / module); only operators with a kit are offered (welcome.diyKitted). Per-browser sp.pref.diy = { v, picks }, synced as room.diy { picks } by the same sync engine; 导出 / 导入 as { kind: 'stronghold.diy', v, exportedAt, count, picks } (ui/diyModel.js, screens/diy.js). In the match a slotted piece is the operator for its player (name, art, class, bonds from its factions, no 特质, the pick's skill and module; the slot's tier, price and merge), sold only in that player's shop from the slot's 调度中心 level with its own stock; the shop / reward / detail cards and the own pieces carry a 「自选」 badge (ui/gameLogic/diy.js); the 0.2.0 自选编队 design subsection has the rules and sources.