- Shared card pool and item stock as in the official mode (the owner's request, from the 路标档案馆 video and public notes): solo low-tier exceptions, co-op operators x2, shared item stock with special-item exceptions, normal / advanced occupancy 1 / 2, re-check on buy, 机变 exhaustion, release on consume / destroy / elimination; 整备 never exceeds stock; invariant held <= cap.
- 突袭: one taunt-first reselect right after a revive (the owner's rule); ordinary polls and redeploys keep the 0.2.3 rule; a revive on an enemy's tile stays. #475: online boss / hidden-boss Raid may land on the empty half (owner-limited scope).
- 浊心斯卡蒂 S1 x 坚守 no longer bounce redirected damage, and the two shares conserve the hit (idea of PR #464 by @HappyTanuki).
- Operator fixes adapted from PRs by @Xinglan233 (#459 哈洛德 S2 elemental trigger, #460 涤火杰西卡 shield on the four adjacent tiles, #461 家族徽章 first hit after stealth, #462 松果 S2 regrow per deploy); 克莱门莎 S2 splash 1.1 / 100% ATK and air auto-trigger (#467); 白面 copying 歌蕾蒂娅 / 余 skips the three-member condition (#473, the owner's video).
- 香槟炸弹 is not chosen by friendly auras / heals (PR #468 / #476 by @Siornya); 琳琅诗怀雅 coin balance (PR #450 by @IceCodeNew); Touch prep display (PR #451 by @YinqiMargatroid); lobby header (PR #471 by @YuriRestia); Japanese UI terms (PR #448 by @scutkane).
- AI-only boss countdown grace, render-failure cache / recovery without restarting a battle, device units kept on a same-field re-entry, DIY detail / skill previews, large-text result buttons.
- golden: battle families equal 0.2.3 except diy-135 / diy-136 (克莱门莎); all 18 release matches move through the shared pool, every flip named in docs/history/0.2.4.md §29.12; two coverage scenarios added.
- Conflicts (the branch started at ae22335c): the four i18n packs (union of keys), game.js DetailPanel props (ops + the
voices' voice=true), detailPanel.js resolveDetail (cultOpts + the voices' tap), match/views.js (both helpers),
docs/PLAYING.md and design/network.md merged line by line against the base. test/match/bosshp.test.js's ATK formula
now includes the unit's 练度 multiplier (cultMul).
- Grok review: MERGE AFTER FIXES — the note and the module card at the set potential fixed on the branch.
- Golden: every owned operator ×1.1 ATK / DEF / max HP by default — roster 49, bonds 46, fields 22, matches 18, diy 134
(stand-in battles and prototype picks unchanged). Broad node --test 5666 (0 fail after the bosshp formula; 2 skipped);
lint 0 errors; typecheck; i18n strict clean (1152 msgids).
- Conflicts (the branch started at ae22335c): spec.js / protocol.js b.progress (resolved from fb7-ui-backlog + leaksBy),
gameLogic.js / game.js exports (device tips + normalizePersonalChoice), docs/PLAYING.md / design/network.md lines
merged against the base, META.md (画卷 note + 教鞭), test/docs-consistency.test.js pin (both fields).
- Grok review: MERGE AFTER FIXES — the HIGH (a forged boss result paid the cards) fixed on the branch (66a228c6), the
META wording (1ab12763).
- Golden: matches 10 (the 杜宾 bot picks and the guard placement); union: coop3-ABYSS-5 differs from HEAD only in ai_2's
dmgDealt (1238011 → 1186269), no outcome change — HEAD's side had moved it, the new placement applies on that path.
- Broad node --test 5623 (5621 pass, 0 fail, 2 skipped); lint 0 errors; typecheck; i18n strict clean.
GitHub PR #281 by @IceCodeNew (this commit is substantially the PR, ported onto 0.2.2-dev; the field-detail range
idea after @BBleae's fork commits c6437f1 / b451bad, credited here instead of the PR's Co-authored-by trailers). The
unofficial skill-range preview the PR first had is gone (the owner's reply of 2026-10-07: only official-like
behaviour); what remains is the range the unit fights with now.
screens/game/range.js inspectRange resolves the open card's unit: an own board piece in prep (its loadout or
stand-in record, its facing — this replaces the prep selection's range effect); in battle or on a scouted board an
ally op / token on the field, at the snapshot's tile, with the live stats' range when there is one; never an enemy,
a dead or undeployed unit, or a scouted bench unit (UnitInfo `area` hand / temp — the tag the server sends since the
#385 fix; renderInfo now keeps it). shared/protocol.js unitStatsEntry gains an optional `dir` (the unit's facing now:
the snapshot tuples carry none and UnitInfo.dir is the facing at send time). tokenVariantFor moves to
gameLogic/loadout.js. Drag, the wheel and the pen clear the overlay.
Added on top of the PR: a test from the server's own scout m.field (board operator ranged, hand / temp none), docs
(PLAYING.md, design/client.md, design/network.md), comments.
Tests: test/ui/inspect-range.test.js (6), test/ui/playtest6_summons.test.js, test/match/unitStats.test.js;
test/ui/inspect-range.e2e.test.js (SP_E2E + CHROME_PATH: run here, 1/1). Golden unchanged (display; m.unitStats only).
GitHub #394 / PR #395 by @Xinglan233. The wheel's capture listener committed the previewed direction on Enter whatever
had the focus, so Tab to 准备 / 设置 / the exit button / ✕ and Enter deployed (the PR let only a focused ✕ keep its
native Enter). Now gameLogic facingEnter decides: Enter on ✕ cancels; on any other control (button, link, form field,
ARIA control) it is swallowed — the wheel already holds Space and the shortcuts; elsewhere it commits the previewed
direction, and does nothing while none is previewed. The wheel (role="dialog") takes the focus when it opens
(tabIndex -1, no focus ring over the screen), so a button focused before the drop cannot swallow the confirming Enter
and the first Tab reaches ✕; nothing restores the old focus when it closes (a restored 准备 would take the next key).
Docs: PLAYING.md's key paragraph. Tests: test/ui/feedback5-hotkeys.test.js (facingEnter and its wiring, 2 new);
test/ui/facing.e2e.test.js (SP_E2E, the PR's drag / Tab flow, 4 cases: run here in headless Chrome, 4/4 pass, 4/4 fail
on the parent — Enter on the focused exit button deployed). Golden unchanged (client only).
- ui/loadoutModel.js: `ops` beside the entries (sp.pref.loadout { v, entries, ops }; an older stored loadout has none =
潜能 6, 精英2 Lv.60): parseStoredOps, opsOf / setOps / resetOps (defaults never stored), sanitizeOps (only what the
server keeps: the roster and the 自选 owned pool); 导出 carries `ops`, 导入 replaces the settings when the payload has
them (an older export leaves them alone; settings alone import); the 已调整 count and 仅看已调整 count them.
- ui/loadoutSync.js: the store's `ops` (setOpsMap persists both); the loadout sync watches entries and ops and sends
room.loadout { entries, ops } (the ops sanitised against chess.json + backups.json; nothing set: no download);
openLoadout loads effects.json (the 练度 multipliers).
- Cards: gameLogic chessLoadout(chess, loadout, getChess, { ops | cultivation, effects }) composes the record at the
operator's potential and multiplies its stats by the 练度 tier (the player's m.private.ops — shop / hand / board cards
and the detail panel; a teammate's unit its UnitInfo potential / cultivate: unitCultivation; a teammate's bond-popup
card the defaults, never the viewer's settings; a stand-in neither); a 自选 card is composed at its pick's potential
(ownDiyPick from m.private.ops, unitPick from UnitInfo).
- Tests: test/ui/potential-ui.test.js (new, 7): storage, helpers, 导出 / 导入, the 已调整 count, the sync (sanitised ops,
settings-only edits, no download when none), the cards (刺玫 413 / 17 at 潜能1, ×1.05 at 精英1, a teammate's unit, a
stand-in), 自选 cards at 潜能1 (凯尔希 392 / 20) and the 练度 on top; test/ui/loadout.test.js expects the envelope's
`ops` and room.loadout's `ops: {}`.
- Conflicts: the stats entry (merged fb7-stats) and the settings button both sit in the lobby / room top bars — both kept;
public/js/ui/settings.js (#321 diagnostics, voiceLang, SettingsButton); docs/PLAYING.md table rows resolved line by
line against the merge base (#368, #354, #333 edits kept); docs/SIM.md / design/network.md snapshot field lists
(resolved + ammo / wolves / neg / stand / standCut); design/client.md Screens line (#368's AI switch kept).
- Grok review: MERGE AFTER FIXES — the shared-field capsule finding fixed on the branch (1a880699).
- Golden: roster 23, bonds 2, fields 11, matches 18, diy 11 — #308 totals / snapshots and #333's shop RNG (union OK);
broad node --test 5473 (5472 pass, 0 fail, 1 skipped); lint 0 errors; typecheck; i18n strict clean.
Seen: the Art 教鞭 (杜宾's strategy) drew three 战术特训 cards and added a random one — a documented simplification (docs/META.md
§2.5: a personal choice would need its own phase, protocol message, timer and AI takeover). Official: act2autochess
choice_event hunter_band_1 is a PERSONAL_CHOOSE; PRTS 下半 记录 §法术 教鞭 "使用后销毁,于3个战术特训的悬赏任务中选择一项". The
owner decided on 2026-10-08 to follow it. Taken from PR #353 (code and tests by @Sukvii), ported to the split 0.2.2 modules.
Now: ctx.offerBountyChoice -> Match.offerBountyChoice keeps up to three different mode-eligible cards (15 of 20 in 标准 /
险境, 20 in 绝境 / 终极; one meta-rng shuffle) in PlayerState.personalChoice, m.private only (same cards after a
reconnect); g.choice { idx, choiceId } confirms it (Match.pickPersonalChoice: owner, id, round, index; a request without
a choiceId is still the public draft, a stale id never falls through to it). No new phase or timer: Ready is refused while
it is open, the prep's deadline picks at random before the temp pieces and Ready, a bot or a 托管 seat picks with the bounty
scorer (botPickCard, the bots' seeded rng; at the prep's start, after the cast, before Ready; the scheduler's fallback
picks at random if a step fails). The client reuses ChoiceOverlay (personal: no order, no turns, the prep's countdown) over
a locked prep, the Ready reasons, the mock harness; six UI strings in en / ja / ko / zh-TW. An eliminated player's offer
goes with it; invariants.js flags an offer outside its owner's own open prep.
Not taken from the PR: its §25.24 in the released docs/history/0.2.0.md (the 0.2.2 section is the integrator's), its edits
of the Ready toggle beyond the personal-choice case, its tail placement of the i18n keys.
Golden: coop2-NORMAL-8-serverrun (ai_0 杜宾 casts 教鞭 from R2 on and now scores the three cards: teamLp 39 -> 34, virtualMs
121400 -> 118400, rng shop 511 -> 483 / bots 18063 -> 18070 / meta 112 -> 103) and coop2-ABYSS-10-boosted (ai_1 杜宾: virtualMs
121650 -> 124650, shop 590 -> 589 / bots 22075 -> 22096 / meta 141 -> 154); both first differ in round 2, every other scenario
and family is unchanged; node tools/golden.mjs --twice is identical.
Tests: items.test.js (three distinct cards, the tapped one applied once, nothing before the confirm, a refused cast keeps
the Art / count / rng / uid, an eliminated player's offer), economy.test.js (Ready refused, the deadline picks before
endPrep, solo prep untimed), connection.test.js (two players' offers, reconnect, foreign / duplicate / old ids, an offline
player's pick at the deadline), bot.test.js (托管 takeover incl. the failing-step fallback), bot-smarter.test.js (the scorer's
pick, deterministic per seed, no rng draw when nothing is pending), followups.test.js (R14 / R15), draft.test.js,
unitStats.test.js (protocol), hardening.test.js (invariant), gameLogic / playtest3 / playtest4 (view model, Ready reason,
ChoiceView, actions), mock.e2e.test.js and playtest2.real.e2e.test.js (browser, SP_E2E=1).
- Conflict: public/js/ui/settings.js imports (store from #321 + VOICE_LANGS) — both kept. test/ui/playtest6-elite.test.js's
source pin of the onDetail line allows the new tap counter (bcee737f).
- Grok review: MERGE AFTER FIXES — the three findings fixed on the branch (bcee737f). Golden unchanged; broad node --test
5334 (the one failure was that source pin, fixed), i18n strict clean, lint 0 errors, typecheck.
- tools/assets/plan.mjs: one voiceTree() per dub — audio.voice (--voice-lang, cn by default) and audio.voiceJp
(VOICE_JP_LANG, always the JP dub, audio/voice/jp/<charId>/cn_nnn.mp3); fetch-assets plans and downloads both
(stats.voiceJpChars), so setup (checkAssets walks every listed URL) fetches both too.
- data/assets.json regenerated with the project's tool (node tools/fetch-assets.mjs --add-only, raw.githubusercontent.com,
107 s): the only change is audio.voiceJp + stats (+2674 files, +89,388,041 bytes, voiceJpChars 191) + hash — the rest
is byte-identical; the JP tree is the Chinese one with the folder swapped (191 × 14 files, every file a valid MP3).
The audio files are not committed (public/assets/ is git-ignored).
- public/js/audio.js voiceLine(): 'jp' draws from voiceJp and falls back to the Chinese line per slot (a JP entry the
manifest lacks) and per line at play time (the Chinese file of the same name when the host lacks the JP one —
same token, same gate); operators without voice (stand-ins, 盟约·辅助干员, summons) stay silent. setVoiceLang().
- settings: voiceLang 'cn' | 'jp' (VOICE_LANGS, sanitizeSettings), the 语音语言 row under 干员语音 (each dub named in
its own language, like the language switch), 语音语言 in the en / ja / ko / zh-TW packs.
- tools/package.mjs FULL_ZIP_JP_VOICE (true): false holds the JP files out of the full zip (and its update zip) —
not missing, not orphans, never in an update's `removed`; setup then downloads them.
- Sizes measured: art download 548 MiB (was 463), full zip ≈ 505 MB (0.2.1: 428 MiB + 76 MiB of zipped JP voice),
unzipped 711 MB (package --dry-run); lite zip unchanged. README, DEPLOY, ASSETS, setup's fallback size updated;
PLAYING voice paragraph; §21.30 "Changed in 0.2.2" note.
Tests: test/feedback7-voices.test.js (new: plan, manifest, settings, package switch), test/ui/audio.test.js (voiceLine,
the manager's dub choice and fallback), test/ui/gameLogic.test.js, test/docs-consistency.test.js — all fail on the parent.
Applied as-is. The client precheck for withdrawing a board operator onto an occupied hand slot now treats that operator's own hand summon stack as a free slot, which the server already accepted. Temp stacks, board summons, another player's tokens, and an item that only carries ownerUid stay HAND_FULL. stack.count is not extra slots. Dropping on a chess still checks the swap first.
Tests: node --test test/ui/gameLogic.test.js test/ui/facing.test.js test/ui/uiFixes.test.js test/match/feedback1-placement.test.js test/match/feedback5-we2-placement.test.js — 116 pass, 0 fail. Before this commit: npm run -s lint — 0 errors (134 existing warnings); npm run -s typecheck — pass; npm run -s check:imports — pass; node --test test/docs-consistency.test.js test/docs-paths.test.js test/version.test.js test/package.test.js — 47 pass, 0 fail. No new UI strings, so the i18n check was not run. No golden update.
PRTS 卫戍协议/帮助 §装备: 「达到上限强行佩戴会改为替换装备:指定一件已佩戴装备替换为将要佩戴的装备,并销毁被指定的装备」,
no exception for the 装备时销毁 items. Before: a normal 博士投影 opened no dialog and its kept item pushed the OLDEST
equipped item out (replaceUid ignored); 盟约之币 and the other consumables kept both items.
- server PlayerState.equip (player/items.js): on a full carrier the consume path takes the pick (replaceUid, else the
oldest) off before onEquip (_takeReplaced), destroys it (onDestroy reason replace) once the effect went through —
the normal 博士投影 takes the freed slot —, and puts it back at its index when the effect refuses (博士投影 on an elite:
nothing destroyed)
- client gameLogic equipReplaces: the replace dialog opens for every item on a full carrier, consumables included,
except an attaching item that completes an item merge and a 博士投影 on an elite; itemAttaches removed
- bots (itemTarget): a consumable whose effect is the same on anyone goes to a carrier with a free slot first (PR #264)
- docs: design/match.md equipment rules, META §2.5, PLAYING 装备 row, research 04
golden (matches only; each traced equip by equip against the parent — the first change of state is a bot's, or the
AI 托管 human seat's, targeted consumable on a full carrier destroying its oldest item): solo-FUNNY-1 (r9 ai_0 随身身份牌),
solo-FUNNY-2 (r9 ai_0 寻呼模块), solo-HARD-2 (r9 ai_0 随身身份牌, r12 简易通讯机), coop2-NORMAL-3 (r9 ai_1 随身身份牌),
coop3-ABYSS-5 (r11 ai_1 随身身份牌), coop4-FUNNY-6 (r4 ai_1 随身身份牌), solo-HARD-9-boosted (r14 ai_0 随身身份牌),
coop2-ABYSS-10-boosted (r13 ai_0 随身身份牌, r14 ai_1 简易通讯机), coop2-NORMAL-14-standins (r9 ai_0 随身身份牌; now won,
14 rounds), coop2-NORMAL-70-diy (r14 h_0 随身身份牌); the bots' free-slot carrier for target-agnostic consumables moves
no scenario by itself; roster / bonds / fields / standins / diy unchanged
No conflicts outside docs/history/0.2.0.md; the §25 intro keeps one sentence per stream (W5AD's line that also named
the hotkeys' §25.15 joined to CB4's).
Gate: non-browser suites 4981 / 4981 (test/i18n-packs 12 new, test/package 1 new), npm run golden ok,
test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok, docs-consistency + docs-paths 31 / 31.
The own half was already the official one (the right-hand player on the right half with the
right_boss_*_camera_param camera, its layout mirrored col c -> 20 - c with RIGHT <-> LEFT, so its operators
face the leader as before: research 09 §1.2, DESIGN §15); the partner's half stood empty and nothing framed
the pair.
Server: in a boss round's prep m.private.bossMate { playerId, side, units } = the partner's board as UnitInfo
on its half (Match.bossMateView, finalAssault.js bossFieldPlacement); markPrivate marks the partner's
m.private with the mover's, so it follows live; a scout of either player (Match.prepFieldMeta) carries both
halves (`mate`), and its signature covers the partner's board.
Client: render/app.js syncMates draws the partner's units read-only ('m:<uid>' views: never picked, dragged
or swept with the own pieces) and the whole boss field is lit; the team panel frames the pair's avatars
(gameLogic teamFrameIds: the players still in by seat, two by two, as prepCamera — the boss field in the
fight) from the round start through the Final Assault / Hidden Core, with the official ui/battle
bg_team_border tinted the autochess green (#4ed8af; a plain ring without the local art); none for a lone
player, an eliminated player, a spectator seat, or 联防.
Overlap with PR #191 (same-field frames, issue #190; not merged): it published m.public bossPairing and
framed the partner alone in gold, 联防 included, plus a banner naming the partner; here the pairing is read
as the prep camera already does, both avatars get the official green frame, boss rounds only.
[ASSUMED] the partner's bench is not shown; one frame per avatar.
Golden unchanged. Tests: test/match/feedback5-boss-pair-prep.test.js (new, 4), the item 55 test now checks
the scouted player's own pieces (the partner's stand on the other half); SP_E2E
test/ui/feedback5-boss-pair.e2e.test.js (new, 2: both halves and the frames in prep and fight; item 55's
eliminated guest on the boss field with the leader). Docs: design/client.md §10 / §15, design/network.md,
PLAYING.md.
- The settings (sp.pref.settings) hold a key map, action → KeyboardEvent.code, for refresh / freeze / level-up /
retreat / sell / ready (the ready key also pauses a solo battle); the defaults are the 0.1.4 keys R F D Q X Space,
so nothing changes for a player who never opens the section. sanitizeSettings keeps each saved key that may be a
shortcut and gives any other its default; not a map, or two actions on one key, gives the defaults.
- One lookup, actionForKey (the character typed first, then the physical key: the 0.1.4 code-or-key match, identical
for the defaults), serves shortcutFor(e, keys) in game.js and the facing wheel; the modifier, text-field, repeat and
overlay guards are unchanged. The shop bar, ready / pause and underframe key hints read the map.
- 设置 → 快捷键 (the one SettingsModal, in a match and on the title screen): each action with its key; click or Enter
waits, the next key binds; Esc cancels (the dialog stays open), Tab leaves; Ctrl / Alt / ⌘ combinations and the
interface keys (Esc, Tab, Enter, arrows, F1–F12) are refused; a key in use is swapped and the line under the list
names both; 恢复默认. Esc stays fixed. Touch-only devices: a keyboard note and a folded list; one column under 768 px.
- Tests: test/ui/feedback5-hotkeys.test.js (map, sanitising, conflicts, lookup, capture, handlers and hints with a
rebound key), test/ui/gameLogic.test.js (sanitizeSettings now returns keys), test/ui/hotkeys.e2e.test.js (SP_E2E=1:
撤退 rebound to W with the keyboard only in the mock match; phone width).
Golden unchanged (client UI only).
m.private.diyBanned (every bond of the operator off this match) is shown under 本局禁用干员
in the INFO_CHECK briefing, the strategy draft's 本局信息 dialog and the in-game 本局信息
tab: ui/gameLogic/diy.js diyBannedPieces, ui/matchInfo.js matchInfoModel diyBanned +
DiyBannedLine (t() with English in public/i18n/en.json). Client only; golden unchanged.
Mon3tr T1 "可以在攻击范围内的地面使用…重构体"; PRTS 战术锚点 特性 "仅可以部署在凯尔希·思衡托攻击
范围外的远程位". tools/build-data.mjs summonRecord: ownerRange also from an owner talent
naming the token, ownerRangeOutside / rangedTilesOnly from the token text. Regenerated
offline: 3 paths (重构体 ownerRange true; 战术锚点 ownerRangeOutside, rangedTilesOnly).
Match: board.js placement class 'high'; PlayerState.summonExcluded in _legal, the
re-orientation lift (+ toast), _roomForOutOfRange, the invariants and the bot; the
client mirror (piecePosition HIGH, summonExcluded, reasons; English strings added).
Golden: no scenario moved.
Ported by hand:
- public/js/ui/gameLogic.js is a re-export shim on feedback5: #185's TERRAIN_TIPS / terrainInfo went into the new
public/js/ui/gameLogic/terrain.js (the barrel gains one re-export line); closesOnFieldPress 'terrain' into
ui/gameLogic/shortcuts.js.
- i18n (docs/I18N.md): every new user-visible string goes through t() with an English entry in public/i18n/en.json
(42, after the 补位 strings): the terrain names / tags / fact table are N_() and translated where terrainInfo hands
them out, the mechanism lines are msgids with their numbers as params (a line with an optional half is two whole
msgids, the list joins use an array param), the card's 地形机制 / 这一格 titles in ui/detailPanel.js. Official EN
terms where the EN client has them (stage_table tileInfo: Active Originium, Mire, Exhaust Grille, Deep Water Zone,
Incursion Point, Protection Objective, Tunnel Entry / Exit, High Ground; Camouflage). The Chinese output is
byte-identical to feedback4's terrainInfo on all 5187 tiles of the 13 stages and on synthetic stages.
- public/js/screens/game.js (MatchScreen still lives there): the import keeps feedback5's names plus terrainInfo /
fieldTile; the terrain stage is the one view.setStage gets, so it includes feedback5's fieldStage — a 联防 field is
played on its own map (GitHub #41), and a tap there explains that map's tile, not the round stage's. The tileClick
handler and render/app.js emitTileClick / ui/fallbackField.js auto-merged in place. terrainInfo reads the stage only,
so a 补位 / 自选 board explains its tiles the same way (their pieces only change a piece's art and name).
- public/js/ui/detailPanel.js: the terrain branch of resolveDetail before feedback5's stand-in / 自选 helpers.
- public/js/screens/title.js (#183): feedback5's LangToggle / t() imports plus GIcon / SettingsModal; the gear
button's aria-label / title are t('设置') (en.json already has "Settings").
- tools/fetch-assets.mjs / tools/assets/downloader.mjs: --add-only (keepExisting, the font skip) beside #24's
--asset-source / SP_ASSET_SOURCE and the shared MirrorPolicy; feedback5's extraOperators / moduleTypes and the
voice plan unchanged.
- data/assets.json: feedback5's manifest plus #134's audio.sfx.battle.leak (after heal, the plan order); stats files
6993 → 6994 and bytes +23 240 as measured on feedback4; the hash recomputed with the generator's contentHash
(c01aa739a2d3 → 6a547fc5c937). A field-wise JSON diff against feedback5 shows only those four fields.
- docs: DESIGN §24 intro and §24.5 / §24.6 as on feedback4 (the rest of DESIGN.md is feedback5's); ASSETS.md,
DEPLOY.md, PLAYING.md auto-merged.
- Auto-merged and checked: audio.js (the sim still emits ['leak', id], server/sim/battle/deploy.js), render/style.js,
textures.js, tiles.js, board3d/materials.js, boardArt.js, tools/assets/*, tools/setup.mjs, extract.py, the tests.
Checks: the PRs' unit tests (audio 22, gameLogic 44, terrain-detail 1, originium-art 4, boardart 15,
board3d-extract 9, asset-network 21, fetch-assets-shrink 4), i18n 9 / i18n-data 8 / i18n-server 4, client-static 313,
docs-consistency 24, kits_layout 6 pass; lint 0 errors (no new warnings), check:imports, typecheck; golden: all 283
scenarios match, nothing regenerated.
Ported by hand:
- public/js/ui/gameLogic.js is a re-export shim on feedback5: #73's DEFAULT_SETTINGS / sanitizeSettings `voice`
hunk went into public/js/ui/gameLogic/settings.js.
- public/js/screens/game.js: MatchScreen still lives there, so #73's onResult (battleRunner 'result' → resultSpeaker /
resultVoiceSlot) and `voice=${combat}` on the DetailPanel auto-merged in place. feedback5's game.js no longer
imported `store` (the split moved its other users), so onResult's store.get() would have thrown into its
try/catch and the 结算 line would never have played: `store` is imported again. `localAsset` stays dropped.
- public/js/ui/settings.js (i18n phase 1): the slider is `label=${t('干员语音')}`, public/i18n/en.json
"干员语音": "Operator Voices" (next to Music / Sound Effects).
- tools/assets/plan.mjs: buildPlan takes #73's charword / voiceLang / voiceSlots and feedback5's extraOperators /
moduleTypes; the template has `modules` and audio { bgm, bossBgm, voice, sfx }. `chars` now includes the 自选 picks,
so the voice loop's comment says so (the next `npm run assets` plans their lines too).
- tools/fetch-assets.mjs: parseArgs keeps --add-only beside --voice-lang / --voice-all.
- data/assets.json: feedback5's manifest plus #73's audio.voice (120 operators, after bossBgm, the plan order);
stats files +1680 and bytes +42 041 118 as measured on feedback4, voiceChars 120; the hash is recomputed with
the generator's contentHash. No other entry changed.
- docs/ASSETS.md: the stats line lists modules and voiceChars. DESIGN §21.30 stays where feedback4 put it; SIM.md's
'engage' line and docs-consistency's §21.30 test auto-merged (game.js / detailPanel.js still hold the pinned code).
- Auto-merged and checked: audio.js, battle/runner.js emit('result'), components.js mic icon, detailPanel.js voice
prop, shared/protocol.js EV.ENGAGE, server/sim/ai.js, tools/assets/audio.mjs / cache.mjs, #73's tests.
Where #73 met feedback5 work:
- 补位: a chess fielded as its stand-in shows the stand-in in the detail card, and audio.js keys battle lines by
UnitInfo spine (the stand-in's model). The 选中干员 line now takes detail.standIn.charId first, so the stand-in
speaks, not the operator it replaces (test/ui/standin-ui.test.js pins it). A 自选 record is already the pick's.
- The sim's 'engage': stand-in and 自选 units attack through the same performAttack, so it fires for them as well.
Golden regenerated: every moved scenario differs only in events.engage (the new event's count; no gameplay change),
proven by a field-path diff against feedback5:
- roster: all 49; bonds: all 46; fields: all 22; standins: all 10 (the stand-ins engage).
- diy: 101 of 109. Unmoved: diy-010/011/012/092/093 (闪灵, 夜莺, 凯尔希 squads: they only heal, and a heal never
engages) and diy-077 (薇薇安娜), diy-080/081 (赫德雷), whose picks make no attack in those battles (already so on
feedback5).
- matches: unchanged.
Conflicts: server/match/bot.js (W4A's comment + W5K's player data view pgd), test/golden/diy.json (regenerated). data/ equals an offline build of the merged tools/build-data.mjs. Golden regenerated on the merged code: against feedback5 the moved roster / bonds / fields / matches / standins scenarios are exactly W5M's per-PR attribution (#160 skill events, #109 deploy-timed lifecycle, #66 灵巧 sources and hidden layers, 83fc408 歌蕾蒂娅 placement), plus coop2-NORMAL-70-diy (fields operators those PRs touch) and diy-051 / diy-052 (events.skill only: Misery's deploy passives, #160); against fb5-merge4 only feedback5's own additions (the diy kits, coop2-NORMAL-70-diy).
Bonds of 自选 pieces checked everywhere membership is decided (the owner's reminder of 2026-10-05): the match's counts through the player's data view (煌 in 炎 and 维多利亚, one member per slot, 绝技 elites), the sim's bond effects through def.bonds (叙拉古 on deployment, 协防 ×1.2 / elite ×1.4), the client's strip, popups, cards and detail card. Golden unchanged.
Tests: test/ui/diy-ui.test.js (15), test/match/diy-bonds.test.js (4), test/i18n.test.js pins screens/diy.js; test/ui/diy.e2e.test.js (SP_E2E=1, run once: pass).
Ported by hand into the split layout:
- 83fc408 board.js placeClass: the player's loadout no longer counts, as in 83fc408. feedback5's 补位 rule stays: a
chess the player fields as its stand-in is placed by the stand-in's record (ps.fieldRecord). positionClass(rec)
takes no module.
- 83fc408 client piecePosition → public/js/ui/gameLogic/placement.js: meleeOnHighGround(rec) on the stand-in or
chess record, no module. The gameLogic.js shim header and deployedModuleId's comment follow; deployedModuleId is
kept, still exported and tested.
- 83fc408 PlayerState._placementOf comment → server/match/player/placement.js.
- 83fc408 tools/golden.mjs layout: positionClass without the module, on feedback5's stand-in body (recOf) and in
diyLayout.
- bonds/core.js (6ab9a06, 8a2e0c7) applied cleanly; it equals 13808e3's plus feedback5's i18n toast. shared/highGround.js
equals 13808e3's.
- data/: only bonds.json (the 阿戈尔 spec text). node tools/build-data.mjs --offline on the merged tree rewrites all
15 files byte-identical.
Golden regenerated: 16 scenarios moved, all by 83fc408. Each fields 歌蕾蒂娅 (normal, or elite with HOK-X / no
module), whom tools/golden.mjs now lays out beside the enemy path like a ranged operator instead of on the busiest
melee tile: roster-010, roster-012, roster-029, roster-040, roster-041; bond-deputShip-high, bond-indomShip-low,
bond-preciShip-low, bond-preciShip-high, bond-suntShip-low, bond-swiftShip-high; boss-boss_2-pair, boss-boss_4-pair,
boss-boss_6-pair, hidden-boss_8-pair, hidden-boss_10-pair. This is the list in 83fc408's own message. With the
阿戈尔 commits reverted the digests are the same, so those two move nothing in this corpus. The standins, diy and
matches families are unchanged: no stand-in or 自选 piece there is 歌蕾蒂娅, 崖心 or 见行者.
public/js/ui/gameLogic.js becomes a re-export barrel. The bodies move verbatim into public/js/ui/gameLogic/*.js (shared, phases, camera, watch, bonds, shop, placement, draft, enemies, format, shortcuts, settings, result, loadout, panel). The barrel re-exports the same 96 names in the same order. isObj and int gain export on shared.js so sibling modules can import them; they stay off the barrel. No importer changes.