Official (excel/audio_data.json, local client charpack char_172_svrash): his normal attack is the Default mode's Combat —
ON_ABILITY_HIT.char_172_svrash.combat p_imp_spear_n, no swing bank; .attack.2 (p_atk_silver_n swing, p_imp_sword_n impact) is
his S3 mode, .attack.1 his S2 mode. Cause: tools/assets/audio.mjs pickUnitSfx took the numbered variants because their files
end in _n (its skill-mode test is by file name). Now an operator whose default mode is the unsuffixed ability (a plain
attack / combat bank of any event) never takes a numbered variant; operators that number their default mode (attack.0 …)
keep the earlier order. Over the 209 operators of the manifest it changes 4: 银灰 (charpack modes Default / S2 / S3), 雷蛇
(attack.1 = S2, lightning impact), 初雪 (attack.1 = her S1-2 mode) and 圣约送葬人 (attack.4 = S3_End).
data/assets.json regenerated offline (fetch-assets --offline --add-only --allow-shrink; JSON compare: exactly the 4 operators'
entries, hash and stats): 雷蛇 / 初雪 / 圣约送葬人 drop their wrong `hit`; 银灰's entry goes too, because his official impact
p_imp_spear_n.mp3 was never downloaded on this machine — one online `node tools/fetch-assets.mjs --add-only` adds it (needs
the owner's OK for that download). Golden: unchanged (the manifest is client-side). Tests: test/feedback5-operator-sfx.test.js
(3; 2 fail on the parent). Docs: docs/ASSETS.md.
- Cause, client: the prisoner skeletons carry three clip sets whose collar light is grey, blinking orange
and red. The official battle prefabs (local client battle/enm_pfb_6.ab: the Graphic component's anim key
-> clip table, the modes' replace pairs, the clip the Spine starts on) draw mode Default (【禁锢】) on the
grey set, mode R (the warning) on the orange set and mode L (【解放】) on the red one. The name-based role
resolver gave 普通囚犯 / 老练囚犯 their unnumbered set — the red one — so they came out of the gate freed;
no FORMS entry existed, so no prisoner ever changed (强壮囚犯 / 拳师囚犯 / 重犯 / 传奇重犯 stayed grey).
- Cause, sim: buff_template_data enemy_confinement[atk_cnt] spends a charge ON_BEFORE_ATTACK (not while
麻痹) and switches to mode R when one is left; enemy_liberty_listener[warning] switches to mode L before
the next attack (PRTS "进行第4次攻击前,切换至解放状态"). archetypes.js prisoner() counted on the 'attack'
hook, after the damage, so the 4th attack still hit confined, and it sent no form.
- Now: prisoner() counts in `before` (an attack 麻痹 interrupts never gets there, PRTS 拳师囚犯), setForm
'warning' (fx 'phase', drawn silently: kinds.js SILENT_PHASES) before attack times-1 and 'liberty' (fx
'liberate') before attack times; 重犯's free-all and 杰斯顿's KO switch the freed ones too.
render/units.js FORMS maps 'warning' / 'liberty' for the six skeletons (普通 / 老练囚犯 *2 / unnumbered,
强壮囚犯 *2 / *3, 拳师囚犯 / 重犯 / 传奇重犯 *_orange / *_red). tools/assets/spine.mjs PREFAB_SPINE_ROLES gives
普通囚犯 / 老练囚犯 their grey *3 set; data/assets.json regenerated (fetch-assets --offline --add-only, JSON
compare: only those two anims and the hash), data/enemies.json too (build-data --offline: only their
attackAnim.clip Attack -> Attack3, same length and strike frame). The 3D board draws the same Pixi views.
- Docs: ASSETS.md (PREFAB_SPINE_ROLES, the prisoner forms), SIM.md (the form fx), design/client.md (FORMS).
- Tests: test/content/feedback5-prisoners.test.js (the six: warning before attack 3, attack 4 hits with
ATK +50 %, the forms reach the client and UnitInfo; free-all; 麻痹), test/render/feedback5-prisoners.test.js
(manifest = the prefab's grey set, FORMS sets, view clips, PREFAB_SPINE_ROLES = the committed manifest) —
both fail on the parent but the 麻痹 pin; test/render/feedback5-prisoners.browser.test.js (RENDER_E2E=1:
three prisoners on the 2D and the 3D board, each model's set follows its sim form).
- Golden: 28 scenarios. Only the warning's new 'phase' fx (the prisoner fell before its 4th attack):
roster-001, roster-003, roster-007, roster-024, roster-033, bond-miraShip-low. The 4th attack now hits
freed, one attack earlier than before, so the fight after it moves: roster-004, roster-022, roster-026,
bond-kazimierzShip-low, bond-raidShip-low, bond-soloShip-low, bond-victoriaShip-low, bond-visiShip-low,
standin-01, diy-024, diy-061, diy-065, diy-066, diy-094, diy-097, diy-104, diy-105, diy-127; only the
per-second snapshots besides the fx: roster-021, diy-017; and the bot matches whose battles field
prisoners: solo-ABYSS-2, solo-HARD-9-boosted.
Ported by hand from PR #211 by @xcdoge:
- render/units.js: FLY_HOVER 0.32 → 1.3, flat (the official fly offset is one model-independent Vector3(0, 0.35, 0) in
the client's character space, whose unit is the standard battle-prefab scale 0.27; an official screenshot measures
1.2–1.4 tiles); enemyModelScaleY multiplies the skeleton's Y scale and the head (bar) height, also in the impostor
path; a mirrorX model flips on top of the direction flip. The body, its HP bar, damage numbers, projectile hits and
skill rings ride `hover`, the shadow stays on the ground; one camera drives the units over the 2D and the 3D board.
- tools/build-data.mjs MODEL_STRETCH_Y / MIRRORED_PREFABS → enemies.json modelScaleY / mirrorX; data/enemies.json
regenerated offline here (the PR's one-line blob was not taken) — JSON compare: only enemy_1112_emppnt /
enemy_1112_emppnt_2 modelScaleY 1.263 and enemy_1196_msfyin mirrorX true.
- tools/local-extract/enemy_model_offsets.py (optional, host-side prefab reader) and
docs/research/12-flying-visuals-official.md (without the claim that the owner said 「都要」 on 2026-10-05 — the owner's
decision of 2026-10-06 is cited; where the quirk enemies appear corrected against 0.2.0's data: 帝国炮火先兆者 is a FLY
special enemy of the mode, 帝国炮火中枢先兆者 comes with the bounty 战术特训·飞行III, 木制瑞印 is in no wave). No
screenshot or extracted art committed.
Tests (from PR #211): test/render/unitview.test.js (FLY_HOVER ≈ 0.35 / 0.27; the same lift for every flyer; a flying
view lifts by FLY_HOVER — added here: its body and HP bar ride the lift, its shadow stays on the ground),
test/render/playtest6-enemies.test.js (the data keys; the 1.263 aspect and bar height; the mirrored model's x sign);
both fail without the renderer change; the 425 render unit tests pass. Docs: DATA §9, ASSETS, design client.md,
research 00 / 12, DESIGN §25.13.5 and the §25.10 table.
Golden unchanged (drawing only; the sim reads neither field).
Ported by hand from PR #209 by @qingjingshenghuo (its one-line data/config.json was not taken):
- server/match/gamedata.js bossPoolShareOf (used by GameData.bossPoolShare and by finalAssault.bossPoolHp for a plain
game-data object): solo × `solo` (1); co-op with `perPlayer` true × `coop` (1) × the players alive at the boss
round's start (bots and AI 托管 seats count, eliminated / departed seats do not; at most aliveFull 4; a count left
out = a full team). The fixed pool stays restorable through config: bossHpScale perPlayer false + solo 0.25 (and its
optional aliveScaling).
- tools/build-data.mjs: the global bossHpScale carries the rule (perPlayer true, solo 1); the modes' entries keep only
bloodPointKey / unaffectedByEnemyScale. data/config.json regenerated offline — JSON compare: only bossHpScale (global
and the 9 mode entries) changed.
- The problem #209 documented: a 胄 drone death is 2 % of the pool and 限伤 cancels a hit of ceil ≥ 300000, so the
hidden 胄 终极 pool at 3 / 4 players (21.6M / 28.8M) would cancel every drone (432000 / 576000). The drone link is
a share of the pool, no hit: Battle.loseHp takes noHitLimit and the link passes the limit [ASSUMED: the official
client checks every damage modifier and PRTS calls it 真实伤害, but which max HP it reads is undocumented]; a part's
传递 and every hit stay under the limit.
Golden (regenerated; why: the pool size):
- fields: boss-boss_1..7-pair/-solo and hidden-boss_8..10-pair/-solo (all 20 boss / hidden scenarios) — a pair
field's pool × 2 (two players), a solo template's × 4 (solo × 1, was × 0.25); boss-boss_1-pair, boss-boss_1-solo,
boss-boss_3-pair, boss-boss_3-solo, boss-boss_5-solo, boss-boss_7-pair and hidden-boss_9-solo now end at the
200 s cap instead of clearing; the two 联防 scenarios are unchanged.
- matches: solo-FUNNY-2, solo-NORMAL-1, solo-HARD-1, solo-HARD-2 now lose the Final Assault (solo leaders have 4× the
HP: team LP 0 with 93088–176395 of the pool left); solo-FUNNY-1, solo-NORMAL-2, solo-HARD-9-boosted (its Hidden Core
too), coop2-NORMAL-3, coop4-FUNNY-6, coop2-ABYSS-10-boosted, coop2-NORMAL-70-diy still win with more leader damage
(pool × players alive; solo-FUNNY-1 / solo-HARD-9-boosted end with 6 / 1 team LP less). Unchanged:
coop2-NORMAL-8-serverrun and coop2-NORMAL-14-standins (one player left at R14: × 1 as before) and the matches that
end before R14; roster / bonds / standins / diy unchanged.
Tests: test/match/boss-hp-scaling.test.js (from PR #209, adapted — every leader and difficulty × 1–4 players and solo;
R14 / R15 leave out two departed players; AI 托管 and bots count, an eliminated player does not; config restores the
fixed pool, the opposite of the PR's test; the plain-object fallback agrees; only the leader's HP scales);
test/content/enemies_bosses.test.js (hidden 胄 终极 at 3 / 4 players: the drone share lands, a hit of the same size is
cancelled — fails without the change). Updated on purpose: finalAssault, balance, bosshp, connection, docs-consistency
(the §20.9 record points to §25.13.4; the data carries perPlayer true), sim/playtest6_limits (the drone share lands
past the line), match/playtest6-bosspool (the 5 % knob halved to keep the 2-player pool's size: that test is about the
pool's arithmetic). Docs: PLAYING §7, DATA, META, BALANCE, SIM, design engine.md / network.md, the §20.9 / §20.10
records, research 00 / 01 / 08, the code comments, DESIGN §25.13.4 and the §25.10 table.
- tools/local-extract/extract.py TOKEN_SPINES (`--only spine/token`): each token's battle prefab in the
battle token packs (pkgrps/btl_pfb_tokens_*.ab; the Windows build has all 39, the iOS build lacks
btl_pfb_tokens_0), the skeleton of its Front (or only) Spine renderer, written to
public/assets/local/spine/token/<id>/ like the enemy models (sized pma atlas, premultiplied pages;
the newer RGBA pages keep their own alpha). The enemy writer is shared (write_spine; the two enemy
models re-extract byte for byte). The skinpack job of 乌尔比安's block is gone: it extracted
nothing, and that token draws no model in the game (nor do 圣聆初雪's, 酒神's, 贝洛内's and
予愿安洁莉娜's hidden tokens: no Spine renderer, no avatar in either install).
- tools/assets/local-token-spines.json (committed, `fetch-assets --local-spines`) → data/assets.json
tokens[id].spineLocal for 35 自选 summons, 凯瑟琳's 爬行号·防护单元 and 凛御银灰's three 风雪之眼; the
manifest is the same with or without the extraction (JSON diff: only the 39 spineLocal entries and
the hash). spine.mjs LOCAL_SPINE_ROLES maps clips the resolver cannot read (电弧's 戴乌 C_Skill1_*,
酒神's 本能的召唤 Loop / End, 白铁's platforms End, 战术锚点 on its Default pose) [ASSUMED where named].
- assets.js spineEntry: a token resolves its spineLocal like an enemy (no web model behind it: the
diamond stays when it is not listed or fails to load). Scale as the web tokens (UNIT.modelScale).
- setup reports an extraction without the summon models; docs: ASSETS.md "Token models from the local
client", DEPLOY.md §6, DESIGN §13 (design/client.md), README.
- Tests: test/local-token-models.test.js (new), test/local-extract.test.js (job table, prefab walk,
alpha merge), test/assets.test.js (the local token models load like the client does),
test/package.test.js (the full zip ships them), test/static-local-art.test.js. Golden unchanged.
README「方式一」lists the two zips: 完整包 Stronghold-Protocol-v<版本>.zip (about 400 MB, 590 MB unpacked, all the art)
and 精简包 …-lite.zip (about 21 MB: setup downloads the art, Spine models, audio, fonts, emotes and 玩法说明 pages on the
first start, about 440 MB, resumable; the local-client art is not in it), and what both leave out; the disclaimer says
only the full zip carries the art; setup's download is about 440 MB (the 0.2.0 manifest, was 270 MB); the disk row and
the English summary follow.
docs/DEPLOY.md: §0 disk (440 MB of art, 65 MB of runtime dependencies in a zip), §1.1 three ways to get the game (full,
lite, git clone) and what the first start does with each, §1.5 updating with the lite zip, §3 Docker builds from a git
clone (the zips have no Dockerfile; 440 MB), §6 a lite install has no local-client art, §7 the packaging tool for
maintainers: what ships, what is left out and why, the checks, what it needs.
tools/setup.mjs LOCAL_ART_COPY_HINT (shown by doctor): copy the local art from the same version's 整合包(完整包) — the
lite zip has none.
npm run package / npm run package:lite (node tools/package.mjs [--lite] [--dry-run [--list]] [--out <dir>] [--force]
[--keep-stage] [--no-install] [--allow-dirty] [--root <dir>]) replaces the maintainer's copy-the-tree script:
Stronghold-Protocol-v<version>.zip and Stronghold-Protocol-v<version>-lite.zip, one Stronghold-Protocol/ folder each.
Ships (an allowlist over git ls-files): server/, shared/, data/, public/ but public/dev/, the start scripts, the tools a
player runs (setup, vendor, fetch-assets + tools/assets, doctor, and what setup starts: tools/local-extract,
crop-board-atlas), the four research tables the Node sim fallback and fetch-assets read (docs/research 03, 05-enemies,
05-maps, 07 JSON — the first cut left them out, so a lite zip's setup could not plan the art), package.json / lock,
LICENSE / NOTICE / THIRD-PARTY-NOTICES, README, CHANGELOG, docs/PLAYING.md, docs/DEPLOY.md; npm ci --omit=dev in the
stage adds node_modules and public/vendor. The full zip adds the files data/assets.json and data/local-assets.json list,
public/fonts and public/assets/local/ — not the rest of public/assets, so art the data dropped (焰狐龙梓兰's 15 files)
never ships. Left out: test/, maintainer tools, scripts/make-windows-bundle.mjs, the other docs and docs/img, research
notes, handoff/, .github/, types/, lint / editor / Docker files.
Checks (also in --dry-run, which writes nothing): 0.1.x's refusal list plus test, .github, AGENTS.md, public/dev,
node_modules/.cache, .env / .venv anywhere; relative and server-mount imports of shipped modules and the player npm
scripts resolve to shipped files; a full zip has every listed art file; no two paths differ only in case; no shipped
file (text or binary, art included) has a /Users/, C:\Users\ or /home/ path or this machine's account name (read from
the OS at run time, SP_PACKAGE_SCAN_USER=0 / SP_PACKAGE_SCAN_NAMES; node_modules for the name only); no shipped tracked
file has an uncommitted change. A build also checks that the stage holds exactly the plan before zipping, and refuses an
--out inside the repository.
Measured on this tree (version 0.1.3 until the bump): full zip 421,421,613 bytes (14,910 files, 589 MB unpacked), lite
zip 22,088,912 bytes (6,430 files, 85 MB); the 0.1.x layout of the same tree (every tracked file, all of public/assets)
zips to 429,644,767 bytes: 569 files and 25 MB unpacked less. Both stages started (npm start, /healthz, assets), setup
--check is green on the full one, and the lite one plans the art offline.
test/package.test.js (9 tests, about 1 s, no network, no npm install): the selection on sample paths and on the real
git ls-files (imports, npm entry points, research tables and spawned tools ship), the refusal list, a dry-run of a
temporary git checkout with fake art (full and lite lists), the scans and guards, and one --no-install build there.
The official data spells one DEC X module 'dec-X' (uniequip_003_aglina) and the others 'dec-x' (WAH-Y is the other
mixed-case type id), and the asset plan stored each under its own case: module/dec-X.png and module/dec-x.png. On
Windows / macOS those are one file, so a release zip built there could hold only one of them and a case-sensitive
server (a Linux VPS) would miss the other — the release packager found it ("planned but not staged"). The local file
is now module/{typeIcon in lower case}.png; the upstream names keep their order (the id as spelled, then lower case).
data/assets.json regenerated offline (node tools/fetch-assets.mjs --offline --add-only). JSON diff: modules.dec-X
/assets/module/dec-X.png -> /assets/module/dec-x.png, modules.WAH-Y /assets/module/WAH-Y.png -> /assets/module/wah-y.png,
hash, stats.files 6979 -> 6978, stats.bytes 458334505 -> 458333444; nothing else.
fetch-assets compares its orphans (what --prune deletes) without case: on Windows / macOS module/WAH-Y.png on disk is
the listed module/wah-y.png and must not be deleted (orphanFiles, exported).
Tests: test/assets-diy.test.js (one lower-case file per type, no two manifest URLs differing only in case, orphans).
docs/ASSETS.md: the module icon file name.
She left the 自选 pool in 0.2.0 (data/backups.json diy.excluded, a collab operator) but her art stayed in the
manifest because fetch-assets refuses to shrink it. Regenerated offline in this worktree, whose public/assets and
public/fonts are the shared ones: node tools/fetch-assets.mjs --offline --add-only --allow-shrink (--add-only so no
file on disk is rewritten and the fonts entry is kept). The run dropped exactly her 163 entries.
JSON diff against the previous manifest: removed chars.char_1048_orchd2, skills.skchr_orchd2_1..3,
skillsById.skchr_orchd2_1..3, audio.sfx.units.char_1048_orchd2; changed hash and stats (files 6994 -> 6979,
bytes 462620448 -> 458334505, chars 210 -> 209, charsWithBack 207 -> 206, spineModels 673 -> 671,
skills 525 -> 522, sfxUnits 484 -> 483); nothing added, nothing else changed.
The first regeneration also added 71 entries like audio.voice.char_134_ifrit = {select: [], place: []}: the 自选
operators' voice lines are planned but not downloaded on this machine, and resolveTemplate kept an array whose lines
were all missing as []. tools/assets/manifest.mjs now drops such an array like a missing leaf (and the entry it
empties); an array that is empty in the template stays.
Tests: test/assets.test.js (the array rule), test/assets-diy.test.js (no diy.excluded operator keeps art in the
manifest). docs/ASSETS.md: 71 自选 picks, 209 operators (206 with Back), 522 skill icons, the empty-slot rule.
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.
tools/build-data.mjs classifyAttack: a trait whose 法术伤害 comes only "技能开启时" keeps
the normal attack's phys (display / AI only — her kit already attacks physically
off-skill). Regenerated offline: 3 paths, the three forms of char_1044_hsgma2 dmgType
arts -> phys. Golden: no scenario moved.
tools/build-data.mjs TRIGGER_ALLY_RULES (PRTS 特殊策略 "技能范围内存在可治疗的我方单位时释放技能")
writes the row as the engine's SKILL_RANGE on the skill range with allies: true; simdata
normalizeSkill passes allies into the def's trigger (skills.js reads trigger.allies).
Regenerated offline: 9 paths, the S3 trigger of 黍's three forms. op-shu.js no longer sets
the trigger. Golden: no scenario moved.
tools/build-data.mjs summonRecord: placeable also needs an owner that shows the summon
(the variants' display source, the owner's displayTokenDict). The S3 marker is the skill's
own object (PRTS 予愿安洁莉娜 S3 备注: the skill deploys it at her initial tile); every other
undisplayed summon is already HIDDEN. Regenerated offline: one path changed,
backups.tokens.token_10071_aglna2_agairp.placeable true -> false. The kit drops its
placed-marker branch (the skill always summons it). Golden: no scenario moved.
tools/build-data.mjs TOKEN_POSITION_CORRECTIONS (PRTS 棋子 部署位置 "全部位", 备注
"游戏内召唤物信息与实际不符(显示为仅部署在近战位)") sets the 棋子 record's position to ALL;
validateAll checks every entry. Regenerated offline: one path changed,
backups.tokens.token_10064_wang_stone1.position MELEE -> ALL. Her stock / 阻回 unchanged
(an owner question). Golden: no scenario moved (none places 棋子).
tools/build-data.mjs tokenTalentDeckBonus adds a summon's talent max_deploy_count /
max_deck_stack_cnt (the hidden "TOKEN数" talent; a module token part of the same talent
replaces it, matched by prefabKey) to its variant deployLimit / deckStack. Regenerated
offline: only data/backups.json tokens of 麦哲伦 (drones), 令 (souls), 白铁 (devices) and
夜莺 (幻影) changed — deployLimit 3 / 4 with SUM-Y stage 3, 2, 3; deckStack 5 (8), 3 (4).
The hand count (player/diy.js placeableTokens) now matches the official one for every
自选 summoner (PRTS 卫戍协议/帮助 "根据召唤物部署数量上限", the owners' talents, PRTS 幻影 备注).
The summoner kits read deckStack / deployLimit as they are (same decks as before).
Golden: no scenario moved (the diy family places no summon pieces).
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.
Battle.leak emits its own 'leak' event, which no sound handled, so every leak was silent; the client now plays battle.ON_ENEMY_REACHED_EXIT (b_ui_alarmenter) with the bank's own one-at-a-time rule, leaks closer than the sound's length sharing one alarm. The manifest gains only audio.sfx.battle.leak. Contributed by @Convey123.
Only the extraction (tools/local-extract) and the manifest pointer change; no official textures are committed and nothing in play changes. Contributed by @Cloudnyco.
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.
Deploy, skill and result lines come from the operators' official voice slots; a newer line takes the channel over (stale callbacks of the replaced line no longer release it), and the result line is spoken by an operator of the player's own battle. Contributed by @Convey123.
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).
Added to the fast subset (7 matches, 62 of 151 scenarios). No existing scenario moved (17 matches and every other family byte-identical).
Test: test/golden-diy.test.js.
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 见行者.
Ported by hand:
- #109 kit hunks → kits/ops (the tier files stay re-export shims): 宴 chess_char_1_18-utage.js, 野鬃
chess_char_1_19-wildmn.js, 砾 chess_char_2_12-gravel.js, 斯卡蒂 chess_char_3_05-skadi.js, 伊内丝
chess_char_4_04-ines.js, 缄默德克萨斯 chess_char_4_16-texas2.js, 耀骑士临光 chess_char_6_17-nearl2.js. The +/- lines
are identical to #109's tier1/2/3/4/6 diffs.
- #109 generic.js against W5D's own-duration passives (一击即退): the deployment window is computed where #109 computes
it, with W5D's case included. Both forms run #109's duration lifecycle (activateOnDeploy, spType 'none', the
skill's own mods), and W5D's 冲锋号令 DP start effect is kept.
- #103 room.js through i18n phase 1: t('{name}邀请你加入卫戍协议:盟约【{difficulty}】') with the translated difficulty
name (the Chinese text is the PR's), plus its English line in public/i18n/en.json.
- data/assets.json: feedback5's manifest plus #72's audio.bgm.combatAlts (after boss, the plan order). files +3 and
bytes +5 037 595 as measured on feedback4; the hash is recomputed with the generator's contentHash. No other data/
file changed.
- docs: DESIGN §5.6 (#109's timed deployment paragraph before feedback5's trigger rules; "Permanent passive skills are
always on"), SIM §7.4 (both generic passive cases).
- Auto-merged and checked: #149 bond-bar fold (still in screens/game.js), #160 skills.js / spine.js, #109 skills.js /
snapshot.js, #66 addon/battle.js (restTile / isDown live in sim/battle/tiles.js), #150 units.js, #72 audio.js /
plan.mjs, #74 plan.mjs / docs.
Where the PRs met feedback5 work:
- 突袭 (W5L, GitHub #49): raidPoll counts a skill that needs no SP while it is on. That is a passive, as before, or a
#109 deploy-timed skill while its window runs. Once the window has ended only the idle trigger is left. Without this
change 缄默德克萨斯 / 宴 lost #49. research 02 §3.18 says so.
- Tests changed to #109's representation: bonds_addon #49 test (kind 'duration', activateOnDeploy) plus a new test
(window over → idle trigger only), and standin.test.js's 预备干员 PASSIVE branch (the skill's mods instead of the
generic:passive buff).
Golden regenerated on the merged code. Attribution: each PR's gameplay change was reverted alone in a scratch copy, and
all three together. With all three reverted the digests equal feedback5's exactly. Every moved scenario was replayed
and fields the unit that explains it.
- #160 only (0.5 s skill events of deploy passives; events.skill only): roster-001/006/025/049, bond-arcaneShip-low,
bond-emptyShip-high, bond-investShip-high, bond-kjeragShip-low, bond-siracusaShip-low/high, bond-victoriaShip-low,
bond-yanShip-high, boss-boss_2/4/6-solo, hidden-boss_8/10-solo, standin-03/05/08/10, diy-005/006 (普罗旺斯 S1,
琳琅诗怀雅 S1/S2, 星熊 S2, 凯瑟琳 S1, Misery S3 as stand-in / 自选 prototype).
- #109 only (deploy-timed skill state and lifecycle; 突袭 技能就绪 limited to the window; generic 一击即退):
roster-003/004/005/021/023/024/039/041/042/046/047, bond-egirShip-high, bond-indomShip-high,
bond-kazimierzShip-low/high, hidden-boss_8-pair, standin-01, coop4-FUNNY-6, coop2-NORMAL-8-serverrun.
- #66 only (灵巧's aura stays only for a killed member, from its body tile): roster-028, coop2-NORMAL-3,
coop3-ABYSS-5, coop4-ABYSS-7, solo-HARD-9-boosted.
- #109 + #160: roster-002, roster-029, bond-investShip-low, bond-raidShip-low/high, bond-suntShip-low,
bond-visiShip-high.
- #66 + #160: roster-008, roster-027.
- #66 + #109: roster-022, roster-040, bond-indomShip-low, boss-boss_2/4/6-pair, hidden-boss_10-pair, standin-06,
coop2-HARD-4, coop2-NORMAL-14-standins.
- all three: roster-010, roster-012, roster-031.
69 scenarios. Every other digest is unchanged.
The owner's decision of 2026-10-05 reverses the one of 2026-10-04 (elite 歌蕾蒂娅 with HOK-Y only) and follows PRTS:
the line is the 钩索师 / 推击手 branch trait (歌蕾蒂娅, 崖心, 见行者, normal and elite) and every module keeps it, so
the loadout no longer matters. Raised with the PRTS source in PR #69 by @sunstricken (its own change covered only
歌蕾蒂娅; its test page is not taken). GitHub #153.
- shared/highGround.js: meleeOnHighGround(rec) reads the trait without a module (traitBase, else trait); no module id.
- board.js positionClass(rec) widens those six records; placeClass(ps, rec) = positionClass(rec) for every server
check (g.move, swaps, merge tile, evictions, audit, invariants). The client's piecePosition mirrors it.
- bot.js: the existing 高台 preference (a 高台 whose range covers the road) now applies to all six; comments.
- Docs: DESIGN §3 and §6.6, DATA (placement), PLAYING §4, META, research 03 C2, code comments. data/ unchanged
(build-data --offline gives identical files).
- Tests: board, feedback1-placement (each holder with default / no module / every module on each 高台, a 重装
refused, client highlights, swap, battle, bots), gameLogic.
Golden: 16 scenarios moved, every one because tools/golden.mjs lays operators out by positionClass and now places
歌蕾蒂娅 (normal, or elite with HOK-X) beside the enemy path like a ranged operator instead of on the busiest melee
tile, which shifts the others: 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. roster-031 (elite with HOK-Y, already
widened) and all matches are unchanged.
Conflict: server/match/player/basics.js imports (W5G's checkNotOwned + W5I's msg / dn). data/i18n/en.json rebuilt on the merged data (tools/build-i18n.mjs --offline): the 自选 data adds texts of CN-only operators and modules the EN client lacks, so coverage is 97.6 % (skills 95.9, talents 95.1); the coverage floors in test/i18n-data.test.js and docs/I18N.md say so. Golden unchanged; i18n / lobby / match / ui suites 1144/1144 after the rebuild, i18n e2e 1/1.
Conflicts: tools/golden.mjs / test/golden/README.md take both new families (standins from W5G, diy from W5H); DATA.md / SIM.md keep both texts. data/ equals an offline build of the merged tools/build-data.mjs. Golden regenerated on the merged code: against feedback5 nothing moved, diy.json is new; against fb5-diy the W5G / W5J / W5L scenarios moved, and diy-004 / diy-006 (the prototype battles with Touch, changed by W5L) — diy-001/002/003/005 identical.
Golden regenerated on the merged code and attributed by reverting each gameplay PR in a scratch tree: #66 alone moves the fights of roster-008/010/012/022/027/028/031/040, the boss pairs (boss_2/4/6, hidden boss_10) and the matches coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, solo-HARD-9-boosted (灵巧's aura no longer comes from merchant exits and retreats; hidden-layer bonds keep their internal layers); #109 changes the skill state of its deploy-timed skills (units / hooks / status events / snapshots); #160 adds the 0.5 s skill events of deploy passives (events.skill only in roster-001/006/025/049). The remaining scenarios listed by the full regenerate are those three effects together.
public/i18n/en.json: Chinese msgid -> English, seeded from PR #70 (@YuriRestia, credited) for every Chinese
literal of the current code (≈830 entries; 966 of the 1 036 literals left for phase 2 already translated),
completed by hand in the official EN terms. public/js/ui/lang.js: boot language (?lang=en|zh, then
localStorage sp.pref.lang, else Chinese; the browser language is not consulted), switchLang, useLang,
LangToggle; main.js renders after initLang and re-renders in place on a switch, and translates server
texts (m.toast / m.ticker frames, room.closed reasons, error codes via ui/toasts.js describeError).
Converted: title screen (English title, wordmark hidden), lobby, room, settings (语言 row, On/Off,
quality, hotkeys), the shell (crash screen, toasts) and DifficultyTag.
tools/i18n.mjs: extract (msgids + untranslated literals per file), check (msgids missing from en.json),
codemod (html`` text / attributes / strings / template literals -> t(), nested strings, N_ for module
level, i18n-ignore lines), seed (fill en.json from a msgid map).
shared/i18n.js: gettext-style t() / tc() with the Chinese text as msgid (untranslated -> Chinese), {name},
{0} and {n|one|other} params, lists, game-data names (dn / tName), setLang / getLang / onLangChange, and the
server message wire format (msg -> wireMessage -> translateWire). Chinese stays the default, English is a
switch (the owner's decision of 2026-10-05).
tools/build-i18n.mjs (build-data.mjs untouched) walks the zh_CN and EN tables side by side and writes
data/i18n/en.json: the official English of every data/*.json text by record id and field. Blackboard texts
are matched by template (the remake's numbers read back and filled into the English template); stage names
are composed; what the EN client lacks falls back to tools/i18n/fallback-remake.json and fallback-pr70.json
(PR #70 by @YuriRestia). Default EN source ArknightsAssets/ArknightsGamedata (has act2autochess);
Kengxxiao/ArknightsGameData_YoStar is archived (2025-11) and has no 盟约 season (--source yostar).
Coverage: 14 922 / 14 931 texts (99.9 %), 99.3 % official; only 9 CN-only 甄选 operator names stay Chinese.
shared/i18nData.js: the overlay format (index-aligned leaves + a 2-char hash of each Chinese source, so a
stale leaf is never applied). public/js/data.js: setLocale('en') downloads the overlay once; get / lookup /
list return localized copies; getRaw keeps the Chinese file; localeName maps names.
Downloaded 1,295 files, 127.2 MB (133,426,089 bytes) into the shared public/assets; no existing file changed or removed. data/assets.json only gains entries: 72 chars (all with avatar, portrait, Front and Back Spine), 38 tokens (avatars; no Spine — upstream has skin variants only), 208 skill icons, 10 sub-profession icons, 102 SFX units, 131 module type icons.