Official: research 09 §3.1 — after elimination the client keeps observing (self_dead_dialog KEEP_WATCH,
DeadAutoObDn { obIndex, state, preparation }, FindFirstAvailObTarget). Until 0.2.0 every phase reset cleared
an eliminated player's watch: in 回合开始 / 机变 / 休整期 it saw its own empty board, in battle the first field.
Server (server/match/match/watch.js): Match.watchPref (viewer → the player of its last manual g.watch: a prep
scout, a single-player field, or the new optional g.watch playerId on a shared 联防 / boss pair field — PR
#189's review point); _watchTargetOf = that player while still in, else the first player still in (seat
order, AI seats included); startRound scouts its board (_followScout, kept through 机变 and 休整期), both
combat modes and the boss fields start on its field, a resync / a spectator joining in prep gets the board,
a followed player who quits hands over at once. Client: the screen adopts the pushed board like a 前往查看 tap
(battle/observe.js followedScout; the stale-field fix of PR #189), keeps it through the prep phases (no camera
flight home and back), a spectator no longer asks for the first player itself; g.watch sends the tapped row.
Overlap with PR #189 (not merged): the same preference chain; differences — the fallback includes AI seats
(a lone human with AI teammates had nobody to follow), shared fields record the tapped playerId.
Golden unchanged. Tests: test/match/feedback5-watch-follow.test.js (new, 9), spectator.test.js updated (the
spectator follows the player it scouted). Docs: design/network.md, PLAYING.md.
Until 0.2.0 the shop kept its old slot count until the next roll (a refresh or the round start), so a
level-up 1→2 (险境: 3→4 operator slots) or 3→4 (4→5; 标准: 5→6) showed no new card.
PlayerState._openLevelSlots (player/economy.js): the extra slots of the new level are appended to the
chess slots (the item slot stays after them), each drawn at the new level; [ASSUMED] the new card follows
the freeze toggle as a manual refresh's cards do. audit.js checks the slot count and that every card
shown before stays in place. Docs: META.md §3 / §7, PLAYING.md.
Golden: matches — all 18 scenarios moved (solo-FUNNY-1/2, solo-NORMAL-1/2, solo-HARD-1/2, solo-ABYSS-1/2,
coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-FUNNY-6, coop4-ABYSS-7, coop2-NORMAL-8-serverrun,
solo-HARD-9-boosted, coop2-ABYSS-10-boosted, coop2-NORMAL-14-standins, coop2-NORMAL-70-diy): every bot
levels its shop and now sees (and draws the shop RNG for) the new card at once, so purchases and boards
part from there. roster / bonds / fields / standins / diy unchanged; no engine error stored.
Tests: test/match/feedback5-shop-levelslots.test.js (new).
Text only: data/chess.json keeps both lines (traitOverride.desc = the class trait or the module's rewrite,
moduleDesc = the added line) and the battle unit keeps the class trait with the module on — the same trait
text, targeting and attack shape as 不装备 for every added-line module (圣约送葬人 REA-Y also rewrites it).
New helpers ui/loadoutModel.js traitLines / fullTraitText; traitText (ui/detailPanel.js) composes the two.
Golden unchanged (no sim change). Tests: test/ui/feedback5-module-trait.test.js (new), loadout-stats updated.
.gm__watching shows at the tap, but the dev mock answers the row's g.watch 40-100 ms later (game-mock.js mockRequest). The test started the battle about 50 ms after the tap. When the answer came after that, the mock took the scouting request as a battle watch, as a server-run server does, and moved m.field and the battle frames to the teammate's field. The strip follows the field on screen, so it rightly showed that teammate, who has no bonds in the mock, and `.bslot .bond` never came (3 of 10 runs at 1920x1080). Each 640x360 touch tap takes about 100 ms, so the answer always came first there. A player never loses their own strip this way: client-side combat (the default) refuses a teammate's field while the own battle runs. Under server-run combat the late tap shows that teammate's battle with their real bonds and the eye tag.
Now the test waits until m.field is the teammate's read-only board before the phase change. It also asserts that the own battle's expanded strip is the player's own (no data-owner). Every other check is unchanged. 10 of 10 runs pass. Golden unchanged.
- docs/history/0.2.0.md §25.14 (also scratchpad draft 0.2.0.aa.md) in the §25 style: the prisoners' models and
timing (§25.14.1), 忍冬 — her default S3 has cast on its 2-2 running range since §25.5's ACTIVE_RANGE (0.1.4 waited
for her 1-tile range; §25.14.2), a bounty burned to death (§25.14.3), 枯朽萃聚使徒's seeds — fixed by 0.1.3 §23.25,
every leaked seed paid the card again in 联防 on v0.1.2 (§25.14.4); the §25 intro list, the owner's decisions, the
§25.10 table, what stays [ASSUMED] (the 联防 payee) and the open polish (enemy prefabs whose default clips differ
from the manifest: 猎狗pro, 大鲍勃, 碎骨, “斩胄之剑” / “破胄之锤”).
- test/sim/feedback5-community-pins.test.js pins the two reports this tree already answers (they pass on the parent):
忍冬 S3 casts with an enemy two tiles ahead and not three, S2 on its 3-12, S1 on her attack; a bounty 使徒's seeds
carry no bounty and pay nothing, the body pays once, and in 联防 only its own leak carries the card.
- Golden unchanged.
- Reproduced (scratchpad scripts): on its own field a bounty enemy the 活性源石 alone burns to death pays its
owner — on this tree and on v0.1.4 (all 96 walking kill-bounty enemies, 战场#04, no operator: 0 unpaid). The
loss is in 联防: a leaked bounty enemy re-enters with `bounty.ownerPlayerId` = the leaker (unite.js
planUnite), who has no per-player entry in that battle. A death without an operator killer fell back to that
owner, and Battle.addCoins dropped the coins. Until 0.1.3 the 联防 field was the round's map, so 战场#04's
活性源石 burned leaked bounty enemies for nothing — the report; since then the 联防 maps have no terrain, but a
无来源 tick, the enemy's own drain or another enemy's hit still paid nobody.
- Official: PRTS 卫戍协议:盟约 决策 "该敌人于对应玩家所属区域倒下时,使相应玩家获得额外资金"; the bounty pays whenever the
body dies (the owner's decision, relayed 2026-10-06; 0.1.1 "paid once, at the final death"). No PRTS exception
for 环境伤害.
- Now: Battle._bountyPayee (battle/economy.js): an operator or summon of a player in the battle that dealt the
blow pays its player (as before, 联防 helpers included); any other death pays the card's owner when that player
fights there (own field, Final Assault / Hidden Core — unchanged), else the player whose half of the field the
enemy fell on (Battle._ownerForTile) [ASSUMED for 联防: no source names the payee of a death nobody caused there].
- Docs: SIM.md §1.2, META.md SETTLE, PLAYING.md (联防 rules), unite.js header.
- Tests: test/sim/feedback5-bounty-payee.test.js — the 联防 case (one helper; two helpers by half; killed by
nobody, 无来源 terrain damage, another enemy, its own HP loss) fails on the parent; pinned: the own-field
活性源石 death, a helper's operator on the other half, a pair boss field. Golden unchanged.
The 0.2.0 candidate's full pass failed at the second drop ('Waiting for selector `.eqr .eqr__opt` failed'). The view
keeps a dropped piece on the drop tile for 1.3 s (render/app/tune.js DROP_PENDING_MS, unchanged since the first import)
and then flies it back in a frame-timed tween. The test picked the item up again 0.5 s after 取消, where it had been
drawn a moment before. Even alone the first try often grabbed nothing (the helper's retry hid it). With few frames
(software-rendered headless Chrome on a loaded machine; reproduced with frames ~600 ms apart) the press grabbed the
operator under the item, the operator dropped on its own tile opened the direction wheel, and the wheel swallowed the
retry. The dialog itself opens and renders as before (补位 / i18n / 自选 not involved).
Now 取消 is also checked to draw the item back on its bench slot, and the next drop waits for that; under the same slow
frames the drop succeeds at the first try. Alone: 5/5 twice; mock.e2e 32/32, devices.e2e 26/26. A test timing flake,
no product change; golden unchanged.
- 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.
garrison_123 「<刷新时>若为本回合首次主动刷新…此干员在整备区时也有效」 counts per piece (§21.1). No source says whether a
merge resets the count; since 0.1.1 _mergeChess carried the copies' highest per-piece round count onto the elite
([ASSUMED], conservative), so two white copies that had fired this round made an elite that did not fire again. The
owner decided on 2026-10-06 that a newly merged elite counts as a new 拉普兰德.
server/match/player/acquire.js _mergeChess no longer carries the per-piece round counters (拉普兰德's refresh count,
garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes, is the only one): the elite starts at 0 like any new piece;
an in-place promotion keeps its count. test/match/feedback1-meta.test.js: the test that locked the old inherit is
updated on purpose (the elite adds +8 on its next refresh, once), and the case "two white copies refreshed this round,
merge, refresh → +8" is new; both fail without the change. Docs: META (counter API, edge cases), PLAYING, the comments
(PlayerState, pieces, effectsMeta, garrisons/meta), DESIGN §25.13.3 and the §25.10 table.
Golden unchanged (no corpus match merges 拉普兰德 after a refresh in the same round).
0.2.0's third tab 自选编队 (WK 0def7ba, merged in d6fd800e — the toolbar check passes on its parent) widened the centre
column of the symmetric grid, and the co-op briefing's countdown + 导出 / 导入 / 全部恢复默认 no longer fit in their half:
they ran 4 px into the tabs at 667×375 (loadout.e2e's toolbar check), 63 px at 844×390 (the countdown on 自选编队) and,
in English, at every size up to 1920×1080. Not the briefing's 本局禁用干员 lines: the overlap is the overlay's .lo-top.
- the grid centres the tabs while both sides fit in equal halves (minmax(max-content, 1fr) minmax(0, auto)
minmax(max-content, 1fr)) and shifts them otherwise; the tab strip scrolls only as a last resort, never overlaps;
- the countdown caption breaks at its spaces (Loadout / Deadline; 调配截止 stays one line);
- tighter tabs at ≤ 1200 px; at ≤ 1100 px the briefing's countdown takes the place of the 已调整 / 同步 chips;
- phones in landscape (≤ 1000 px, up to the 932 px ones): no chips, and 返回 / 全部恢复默认 (全部持有, 全部清空 — each
confirms first) show their icon, the text kept as their accessible name. Desktop zh looks as before.
Checked: zh and en × briefing and lobby × the three tabs × 18 sizes from 667×375 to 2560×1440 — no overflow, overlap
or offscreen button, the tabs never scroll. The e2e toolbar check now runs zh and en at 667×375 and 844×390 (and en at
1920×1080) and asserts the three tabs show in full; it fails on the old CSS. Golden unchanged.
Its fake predicate turned true 95 ms after the page was made and the test wanted at least 4 slices of 30 ms. On a loaded
machine the 30 ms timers fire late and merge the slices: the 0.2.0 candidate's full pass got 'sliced (3 calls)'. A
preload that blocks the event loop 9 ms of every 12 reproduces it 5 of 5 times. Now the predicate holds on the 4th call:
exactly 4 calls, each with the same predicate, args and polling, each slice 0–30 ms, and the wait lasted the three
slices (≥ 85 ms). It passes 5 of 5 under that lag and under 25 ms of every 27. A helper that stops at the first
timed-out slice still fails it. A test timing flake, no product change; golden unchanged.
Seed 22's only 麻痹 source was one 5 % 催泪瓦斯 on 洛洛: a single hit at 0.1.4, and since WE1's enemy damage frame
(89f730d0, server/sim/ai.js enemyAttack; it re-timed every fight) none of its rolls hits — 0 attacks in the 17 s boss
fight, 12 in the hidden one. The item, the status and the fx work: forced to 100 % the hidden field shows 26 麻痹 and
8 瘫痪. Every other check (digest / pool / fx / 限伤 / drones / 奥术) still runs on the unmodified specs. With the copies,
瘫痪 occurs on every field of all 19 seeds (of 1–120) that pass the other checks (seed 22: 4 / 7 / 29 / 13); with the
palsy fx removed from the sim the test fails (0,0,0,0). A test fixture fix, no product change; golden unchanged.
PRTS 卫戍协议:盟约 下半, 3月27日更新#2调整 (revision 439189): 「[Ⅳ阶]华法琳:赋予的特质的叠层上限从 初始12/精锐24
降低至 初始7/精锐14」 — a nerf; the current 参战干员 record says 至多7层 / 14层 and the client blackboard is 7 / 14.
Research 02 Addendum 1 had read the line backwards, so the remake overrode garrison_95's cap to 12 / 24.
Ported from PR #192 by @kukiC: GRANTED_CAP_OVERRIDE is removed from server/sim/content/garrisons/battle.js and from
the client-result layer bound (server/match/fields.js layerAllowanceOf); a granted trait keeps its data cap like a
native one. Tests from the PR: garrisons_battle (updated on purpose: the 7th cast reaches 7 / 14, the 8th adds
nothing), feedback1-gaps (the bound with 华法琳 + 古米), playtest3 (both cards show 7 / 14); the first two fail without
the fix. Docs: research 00 / 01 / 02 corrected (02-bonds.json's note; data/bonds.json regenerated offline — only
miraShip.spec.layerGain changed), history 0.1.1's pointer, DESIGN §25.13.2 and the §25.10 table.
Golden unchanged (no corpus battle casts 8 times in front of 华法琳).
The item text 「若已拥有至少2名该初始干员,则再获得1名该初始干员;否则随机获得1名同盟约初始干员」: the 否则 is the
case of fewer than 2 owned. builtinMeta's use_equip_reward_char_chess returned only when grantChess(base) gave a piece,
so an elite 溯光星源 plus 2 normal copies (all 5 Ⅵ阶 copies) fell through to the same-bond roll (白面鸮 in the report's
seed). Now with 2 owned it asks for the 3rd and stops; the item is still consumed. The bots' carrier choice
(itemTarget) skips 2 copies whose pool is out.
Tests: test/content/items.test.js (the report with the normal and golden item; 2 copies with the pool drained; 2 copies
with copies left still merge), test/match/bot-smarter.test.js (no carrier on a pair whose pool is out); both fail
without the fix. Docs: META built-ins and bot items, research 04 / 06, DESIGN §25.13.1.
Golden unchanged.
- 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.
New on feedback4 since the last merge: W4D (bbe59b9, merged as 7ee815e) and the 0.1.4 version commit (30e7ace).
feedback5 keeps its own package.json / package-lock.json / APP_VERSION / README / DEPLOY version lines (0.2.0 sets
its own version later) and takes feedback4's CHANGELOG 0.1.4 entry, with W4D's line in it.
- Port (PRTS 异常效果 SLEEPING 沉睡 = 无法行动+无敌+不可阻挡; GitHub #140; the owner's decision of 2026-10-06):
`_checkBlock` refuses `flags.sleep` (battle/blocking.js) and `applyStatus` releases an enemy that falls asleep
(battle/status.js). Battle.js stays feedback5's façade, and tier3.js stays deleted (the 瑕光 comment is in her kits/ops
file). ai.js: WE1 (89f730d) cuts a swing before enemyAttack on a stun / freeze / sleep. The cannot-act branch is still
there, and a plain-buff `flags.sleep` still makes the enemy `stun` (units.js), so the plain-buff release stays in that
branch, after the cut, with its comment rewritten for the new order.
- New on feedback5: 酒神's 迷狂牢笼 (kits/ops/op-phatm2.js cageBlock, O22) duplicated the old three-flag 不可阻挡 check.
It now skips a sleeper too. No golden scenario spawns a 牢笼 over a sleeper: the corpus is identical with that line
reverted. 予愿安洁莉娜's S3 helper is unchanged, since canTargetEnemy already drops sleepers.
- Readers that follow without code: blockedTargets (blocked-first targeting, the DEFAULT trigger, hitAllBlocked), the 要塞
branch of acquireTargets (u.blocking), “庞贝”'s clock (blockedBy), the snapshot's UF.BLOCKED and the client's block
marker (render/units.js). 死芒 S2 折朽 (自选) lets go a linked enemy that her 悲叹的仆役 held.
- Docs: DESIGN §24.9, its §24 intro entry, the §5.5 Blocking sentence (on feedback5's 阻挡半径倍率 text) and §22.8's
"(since §24.9)", all as on feedback4; SIM.md as on feedback4 (the plain-buff sentence merged with feedback5's
noNewBlock / undying text).
- Golden: a probe (counting successful sleeps on a blocked enemy, plain-buff sleeps and 牢笼 spawns over sleepers) ran
over all 283 scenarios. Every scenario that moved sleeps blocked enemies; the counts are in brackets:
roster-003 (小满 S2 [5]: same fight, blocked 27 → 29, snapshot hashes); roster-005 (elite 小满 S2 [3]: the 140 s
timeout with 83 kills and 1 leak, where it used to clear at 135.43 s with 84); roster-013 (缇缇 S3 [11]: same kills,
blocked 22 → 33); roster-032 (缇缇 S1 [10]: 71 kills instead of 68, 8 leaks instead of 11, 4 knock-outs instead of 8,
萨尔贡 layers 41 → 35); roster-038 (elite 缇缇 S2 [1]: ends at 197.40 s instead of 197.27 s); roster-039 (瑕光 S2
normal + elite [31 + 12]: same kills and counted leaks, the 窃贼 gets away, blocked 53 → 87); roster-042 (缇缇 S2 [3]:
ends at 168.77 s instead of 166.93 s); bond-maniShip-high (小满 [1]: blocked 29 → 30, snapshot hashes); solo-FUNNY-1
(ai_0's 缇缇 [3]: only ai_0's damage dealt); coop4-FUNNY-6 (ai_0's 缇缇 [15 + 56]: from round 9 ai_0's 精准 / 萨尔贡
layers and boss damage move, ai_1 loses no LP from round 11, team LP 124 → 130); coop4-ABYSS-7 (ai_2's 小满 [18]: ai_2
one kill fewer, ai_0 one more LP lost in round 9); coop2-NORMAL-8-serverrun (ai_1's 缇缇 [83]: only ai_1's damage dealt;
WE1 had already turned this match into a loss); coop2-NORMAL-70-diy (the 自选 seat h_0's 缇缇 [171], ai_0's 缇缇 [74]
and 小满 [19 + 21]: h_0's layers move from round 4, then its AI 托管 buys differently (boards from round 7); ai_0 246
kills instead of 233, same end); diy-131 (死芒 S2 at tiers 5 and 6 [1 + 1]: snapshot hashes only); diy-132 (elite
死芒 S2 [2]: ends at 199.50 s instead of 198.13 s, same kills). One scenario sleeps a blocked enemy and did not move:
coop2-HARD-4 [1]. ai_1's 小满 lets go the enemy it holds; another enemy takes the slot; the sleeper wakes with no room
and is blocked further on by 2_16_a. The match digest has no snapshot hashes, and its kills, leaks, damage and funds
are identical. fields and standins did not move. Most outcomes differ from feedback4's list because feedback5's
baselines differ (WE1's damage frame, etc.).
- Tests: test/sim/feedback4-sleep-unblock.test.js (from feedback4) passes 5/5 on the split layout. Every case fails with
the three sim hunks reverted, and case 3 (plain buff) fails with only the ai.js release reverted. Targeted sim tests
pass 184/184 (blocking, block targets, enemy swing, feedback2-enemy-attack-stand, combat / statuses, robustness,
stun SP, professions, healers, fidelity …). Content tests plus kits_layout and docs-consistency pass 658/658 (kits
t1t2 / t3 / t5 and alt t2 / t3 / t5 for 小满 / 瑕光 / 缇缇, 酒神, 死芒, garrisons, bosses, items). Stealth / liftoff /
enemies pass 36/36. npm run golden: all 283 match. GOLDEN_FULL=1 test/golden.test.js passes 9/9 (no stored engine
error). lint 0 errors, check:imports and typecheck clean.
- GitHub #140: a player tested 瑕光's S2 in the official mode — she slept 4 enemies and still blocked the next ones,
after the skill no more were (「被沉睡的敌人是不占用阻挡数的」). PRTS 异常效果: SLEEPING 沉睡 = 「无法行动+无敌+不可阻挡」,
BLOCK_FREE 不可阻挡 = 「无法阻挡/被阻挡,自动解除阻挡」. Until now an enemy that fell asleep while blocked stayed blocked
and kept its blocker's slot.
- Battle.applyStatus releases an enemy that falls asleep (as for 浮空 / 恐惧 / 不可阻挡), Battle._checkBlock never blocks a
sleeper, and ai.js updateEnemy lets a plain-buff sleeper go on its next update. The sleeper stays put; awake, it is
blocked again only by a blocker with a free slot, else it walks on. A stun, a freeze or a refused sleep keeps the
block; sleeping operators are unchanged.
- Readers of the block follow: Battle.blockedTargets (blocked-first targeting, the DEFAULT trigger, hitAllBlocked)
drops the sleeper — of the hitSleep attackers only 瑕光 blocks, and her 仁慈 reads her range; a 要塞 holding only a
sleeper no longer stalls; “庞贝”'s self-blast clock restarts when a sleep ends its block [ASSUMED]; the snapshot's
blocked bit follows.
- Tests: new test/sim/feedback4-sleep-unblock.test.js (5 cases, each fails without the change: the slot frees and the
next enemy is blocked, the sleeper stays put; awake with no room it walks on; never blocked while asleep, re-blocked
on waking beside a free blocker; a plain-buff sleep; the real 瑕光 S2). 1274 targeted tests (blocking, statuses,
小满 / 瑕光 / 缇缇 kits, garrisons, professions, enemies, kits t1–t6), docs-consistency and GOLDEN_FULL pass.
- Docs: DESIGN §24.9 (and the §24 intro, §5.5 Blocking, §22.8's release list), SIM.md (Blocking, the `sleep` row,
plain buffs), the buffs.js / Battle.js / ai.js / 瑕光 kit comments.
- Golden (every moved scenario sleeps blocked enemies; DESIGN §24.9 gives each move): roster-003 / roster-005 (小满 S2:
82 kills instead of 83 / cleared at 136.73 s instead of the 140 s timeout), roster-013 (缇缇 S3: only the blocked
count and the snapshot hashes), roster-032 (缇缇 S1: 7 knock-outs instead of 5), roster-038 (elite 缇缇 S2: ends at
197.43 s), roster-039 (瑕光 S2 normal + elite: 41 of 52 killed instead of 47 — the sleepers walk on when they wake),
roster-042 (缇缇 S2: ends at 161.17 s instead of 166.97 s), bond-maniShip-high (小满: only the blocked count and the
snapshot hashes), coop4-FUNNY-6 (ai_0's 缇缇: layers and boss damage from round 9), coop4-ABYSS-7 (ai_2's 小满:
rounds 11–12), coop2-NORMAL-8-serverrun (ai_1's 缇缇: one more leak in round 10, the team eliminated in round 13
instead of winning). fields unchanged.
Docs only (golden unchanged): §5.5 Enemy AI — every enemy attack strikes at its clip's damage frame after its swing
starts, a stun / freeze / sleep / 浮空, hiding, 缴械, 恐惧 or 战栗 before the frame cuts the swing; §5.5 Operator attacks —
hitAllBlocked takes up to the block count (never fewer than 1). Both normative lines were left stale by those commits.
Also: the header of test/content/feedback5-lowhp-heal.test.js names its source (the local client's data) plainly.
PRTS 伺夜 S2 备注 「仅场上存在狼群,且狼群未获得此技能的充能时可触发技能」 uses S1's words, whose ① counts the 战术点形态 — the
follow-up's recommended reading [ASSUMED].
- vigil kit: S2 casts when ready while the pack is on the field in either form (packOf) and holds no unused gift; in the
form it pays +cost DP at once and the gift waits on the pack for its next attack once back (the heal reaches a standing
pack only). It used to wait for the pack.
Golden: no scenario moved (no corpus battle has S2 ready with the pack in its form).
Tests: kits_alt_t3 "3_19 伺夜 S2" updated on purpose (cast in the form, DP, the waiting gift); kits_t3, tokens_devices pass.
Docs: DESIGN §5.6 trigger note, PLAYING.md 技能自动释放.
The 补位 stand-in Misery (char_615_acspec): S1 物理的服从 and S2 战争的恭顺 are PASSIVE / ON_DEPLOY with a 10 s duration;
PR #109's deploy-timed contract runs such skills as duration skills started by every deployment, with their own
skillStart / skillEnd (as 宴 S2, 斯卡蒂 S2, 缄默德克萨斯 …).
- standin-acspec.js: S1 / S2 = kind 'duration', activateOnDeploy, no SP, trigger NEVER, their mods for the duration;
二象命末 reads "S1 running". S3 空间的归依 stays a passive with its deploy hook. Numbers unchanged.
Golden: standin-01, standin-06 (Misery for 缄默德克萨斯, S2): her skill casts 0 → 14 / 12, skillStart / skillEnd counts,
'skill' events and snapshot hashes; every damage, kill and HP unchanged. Others unchanged.
Tests: test/content/standin_acspec.test.js updated on purpose (the S1 / S2 lifecycle); golden-standins, backups pass.
The client's buff_template_data: charge_token[born] = ON_OWNER_BORN → RechargeToken (timing NORMAL); the summon's own
charge_token[finish] = ON_OWNER_FINISH → RechargeToken (timing ON_FINISH) — PRTS 镜中虚影 "退场时返还1个可部署的镜中虚影", PRTS
傀影 备注 "部署召唤物后立刻开始计算再部署时间". Reading [ASSUMED: from the enum and the owner's first deployment]: NORMAL = usable
now, ON_FINISH = after the summon's own redeploy time — the reading of every other summoner kit (谬因, 机械师, 涤火杰西卡,
麦哲伦 / 令 / 电弧, 白铁, 娜斯提, 凯尔希).
- op-phatom.js: his (re)deployment (not a free move) readies a waiting twin at once (it comes back at the next retry,
paying its cost); its own timer from its last deployment still holds it while he stands. op-kalts.js: the comment cites
the shared reading.
Golden: no scenario moved (no corpus battle redeploys 傀影 while his twin waits).
Tests: test/content/feedback5-summon-recharge.test.js (new, 2); op_phatom, op_kalts pass.
On 战场#08 (act2 m04) the jump can land on tile_fence_bound (LOW, buildable ALL: deployable,
attacks, blocks no ground enemy) when the enemy stands in the 深水区 / on floor next to the
fenced blocks; the board draws the fenced region raised, the likely 「高台」 of the report.
No rule change (the official landing does not say it avoids 围墙). The test runs a melee
member's jump on every battle stage and enemy tile: low ground only.
The owner's ACTIVE_RANGE rule (a MANUAL skill whose running range strictly contains the
operator's own) for two skills whose running range is no data range (O1, O11 reports):
op-aphris.js adds the beam's path as a content trigger range; op-vvana.js switches S3 to
ACTIVE_RANGE on 3-2 once its first cast of a deployment ended (DEFAULT again on redeploy)
through a new additive SkillRuntime.setTrigger(rule, grid). The scan of the wave 2-3
reports and the 41 candidate skills is in the doc draft (others: target-centred, already
fixed, or owner questions). Golden: diy-045 (谬因 S2 fires for enemies beyond her 6 tiles)
and diy-078 (薇薇安娜 S3: skill timing in hooks / snapshots only) moved.
PRTS 分支特性信息 链愈师: "跳跃范围为x-4…不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。
可选择满生命我方单位为跳跃目标,但仍受禁疗制约" (range_table x-4 = the 3×3 around the unit).
- ai.js chainHealNext: an ally on the 3×3 of tiles around the last one healed, not yet in the chain, no device / hidden /
孤立 / 禁疗 unit (the healer's healThrough summon excepted), the lowest HP ratio first — a full-HP ally too, the chain
goes on from it — then the latest deployed. doHeal (莎草) and Mon3tr's chainFrom both use it (her fullProsts case is
now general and gone). It used to be the most injured ally within 2.5 tiles. [ASSUMED] 禁疗 = not selected.
Golden: roster-004, roster-005, roster-023, roster-024, bond-sargonShip-low (莎草's chains: other jump targets, full-HP
links); diy-133, diy-134 (Mon3tr). Others unchanged (no match fields a chain healer).
Tests: test/sim/feedback5-chain-heal.test.js (new, 3); op_monstr, kits_t1t2, feedback2-surtr-stealth pass.
Docs: SIM.md §Professions (chainhealer row).
bot.js: the player's own slotted piece scores DIY_PIECE_BONUS (12) more; levelUp takes the
step that opens a slotted tier with copies left one round earlier (diyOpensAt). Bots field
no 自选 piece. Exports buyScoreOf / levelUp / diyOpensAt for the tests. Docs: DESIGN §6.6,
PLAYING. Golden: coop2-NORMAL-70-diy moved — the 自选 seat under AI 托管 reaches level 6 one
round earlier and fields its tier-6 Sharp at round 12.
PRTS gives only the display text; the client's module buffs decide (local [uc]equips.ab + buff_template_data, extracted
2026-10-06): 瑕光 / 古米 / 塞雷娅 GUA-X set_heal_scale_by_hpratio (FilterByTargetHpRatio LT), 录武官 reckpr_e_002_tr (LT),
Touch acmedc_e_tr (LT); 华法琳 bldsk_e_002_tr and 闪灵 shining_e_003_tr — like 黍's GUA-X and 凯尔希's PHY-X —
heal_scale_up[hpratio][LE].
- kits/shared/tier4.js installLowHpHealBonus and tier5.js lowHpHealUp take { atOrBelow }; 华法琳 (tier 4 and 5) and 闪灵's
PHY-X heal ×1.15 at or below 50 %; the strictly-below kits keep `<` and cite their template.
Golden: no scenario moved (no heal of the corpus lands on an ally at exactly 50 % HP).
Tests: test/content/feedback5-lowhp-heal.test.js (new, 2); kits_t4, kits_alt_t4, kits_t5, kits_alt_t5, op_shining,
standin_acmedc pass.
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.
gamedata_const ba.protect / PRTS 术语 庇护 "受到的物理和法术伤害降低相应比例(同名效果取最高)": the strongest source holds; the
自选 kits already used battle.applyStrongest('protect'), these six multiplied private cuts with it.
- kits/shared/tier1.js: PROTECT, protectMods, PROTECT_TICK_HOLD and holdProtect (applyStrongest on the shared key).
- 宴 / 赫拉格 / 左乐 (below their HP threshold): refreshed every tick and at each hit on them; 余 (blocking) and 缪尔赛思's
melee copies at their old cadence; 赫德雷's 庇护 joins the key, his CRU-X 物理伤害 + becomes its own instance. 左乐 keeps the
PRTS S2 ordering (his own 庇护 newer than his barrier cuts behind it); 行险 drops his own held 庇护 for its 0.1 s
re-judgement [ASSUMED: another source's returns with its next refresh]. 遥's bubbles stay separate (the client's
damage_resistance[bonus] multiplies with the common damage_resistance[inf]).
Golden: no scenario moved — no unit of the corpus holds two 庇护 at once, and one source alone cuts exactly as before.
Tests: test/content/feedback5-protect.test.js (new, 2); kits_t6 (余), kits_alt_t6 (缪尔赛思), op_hodrer updated to the
shared key; kits_t1t2, op_helage, op_zuole pass. Docs: SIM.md §3, kits/README.md checklist 9.
「自选干员放入后模拟中的补给池随机范围也将被相应扩大」: player/diy.js diyStockEntries (every
slotted piece with copies left; the roll's own tier rules, no 调度中心 gate) joins
effectsMeta ctx.rollChess / ctx.rollPool (spDraft rollPool extra + chessOf: bonds through
the player's view) and the 驰援 fallback in choices.js. Granted from the player's own
stock (poolOf). Docs: META, PLAYING. Golden: no scenario moved.
garrison_90 (bond_actived_maxstack, "当前已激活且层数最多的盟约层数+1(每场作战至多10层)"):
fireGain keeps the instance's count and passes the rest uncapped to gainLayers, so a
change of the highest bond no longer grants a fresh 10 / 20 (PR #178 review). Other
bond types keep the per-(instance, bond) cap. Local to garrisons/battle.js.
Golden: no scenario moved.
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.
PRTS 分支特性信息 武者: "特性治疗于干员每次输出伤害时触发(不局限于攻击)", "通过自身特性/天赋/技能产生的作用于自身的治疗效果会无视
自身的禁疗" (utage_trait / helage_trait / zuole_trait: ON_OUTPUT_DAMAGE); PRTS 异常效果: an abnormal flag is one switch.
- professions.js installMushaHeal: one heal of the trait value per damage instance dealt to an enemy, at that instance
(every hit of an attack, skill / item / bond damage); a dodged hit, a 流失, an element 损伤 or a talent / DoT damage heals
nothing; ignoreHealFree. 收割者 keeps its rule (installReaperHeal) and ignores 禁疗 too (the same PRTS sentence).
- 赫拉格's kit: its second-hit heal patch is gone (redundant). 左乐's S1 reads his HP when it fires, before its strike's
own heal [ASSUMED].
Golden (diy only): diy-007, diy-008, diy-009 (赫拉格: a multi-target attack heals 70 per target in separate heals instead
of one 70 × n heal — HP identical, heal hook / event counts move); diy-033, diy-034, diy-035 (左乐: heals land between
his S1 / S2 hits, so his HP-dependent T2 roll, 坚忍 and 庇护 read the healed HP from the second instance on). roster,
bonds, fields, matches, standins unchanged (宴 hits once per attack).
Tests: test/sim/feedback5-musha-heal.test.js (new, 4); op_helage, op_zuole, kits_t1t2, kits_t5, kits_alt_t5 pass.
Docs: SIM.md §Professions.
PRTS 黑键 S2 备注 "攻击范围内不存在可部署位时,技能不会被触发" (skill prefab _minTileNum 1):
the kit casts it (trigger NEVER + a tick: ready, able to act, one free deployable tile
in his range — hasRemnantTile, no rng) instead of the data's DEFAULT waiting for an
enemy. Checked and kept: 温蒂's cannon (summons follow the basic strategy, PRTS
"对于拥有手动释放技能的召唤物,亦会强制开启技能"), 凯尔希·思衡托 S2 (a medic's target is an
injured ally; the owner's ACTIVE_RANGE on her y-11).
Golden: diy-114, diy-115, diy-116 moved — the scenarios fielding 黑键 S2 (his S2 fires at
full SP from the battle start).
PRTS 状态机: ATTACK / COMBAT "每帧检查异常状态,若有则切换到异常状态的状态"; PRTS 异常效果 STUNNED (the state machine
leaves the attack), DISARMED "正在进行的普通攻击将被中断". The strike frame is data/enemies.json attackAnim.hit.
- ai.js: attackWindup = the clip's strike frame (half the clip when none is named), shortened with the clip like the
stand; the cooldown counts down to the strike and the swing (enemy.swing) starts when it reaches the wind-up with a
target in reach (attackTargets, shared with the frame) — steady-state strikes stay one interval apart. An enemy whose
cooldown ran out before it had a target (walking, just blocked, a cut swing) swings from the start: the whole wind-up.
- A stun / freeze / sleep / 浮空 / hiding, 缴械, 恐惧, 战栗 while blocked or losing every target cuts a swing short of its
frame: no strike, the next swing starts from its wind-up. A stun in the cooldown only pauses it; a strike already
made stays made. No clip known (36 of 249 kinds): no wind-up, the old rule. [ASSUMED] targets re-taken at the frame,
a cut swing does not spend the cooldown, enemy heals keep no wind-up.
Golden: 275 of 283 scenarios moved — roster all 49; bonds 41 of 46 (unchanged: bond-arcaneShip-high,
bond-kjeragShip-low, bond-maniShip-low, bond-skillfulShip-high, bond-swiftShip-low); fields all 22; matches all 18;
standins all 10; diy 135 of 138 (unchanged: diy-027, diy-029, diy-087) — every battle where a clip-carrying enemy meets an
operator: the first strike of a contact waits its wind-up, stuns cut swings (the cut alone moves 55). With every
attackAnim stripped from the data the corpus is byte-identical to the previous commit (nothing else moved). Bot
matches: solo-HARD-2 now wins, coop2-NORMAL-8-serverrun now loses; the other 16 keep their result.
Tests: test/sim/feedback5-enemy-swing.test.js (new, 6). Updated on purpose: feedback2-enemy-attack-stand.test.js (it
locked the instant first strike), enemies_bosses.test.js (沉沙 / 新硎's cleared interval, 帝国的甲胄, 斩胄之剑 / 破胄之锤 wait
for the frame). test/sim, test/content, test/match pass. Docs: SIM.md, DATA.md (attackAnim), ASSETS.md.
shared/diy.js DIY_EXCLUDED_MODULE_TYPE = /^(?:ISW|SO|RA)-/: modules whose own effect works in
another game mode only ("在【岁的界园志异】中", "在【沉沦者的黑流树海】中", "在生息演算中"; the
excel gates those parts roguelike / sandbox) — 电弧 SO-A / SO-B, 机械师 SO-A, 森蚺 RA-A
[ASSUMED, the ISW-A reason]. validateDiyPicks refuses them, checkDiyPicks drops such a pick,
the picker hides them; the record and the kits still field them. Docs: DATA, PLAYING,
DESIGN §8. 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).
PRTS 分支特性信息 秘术师: "能量储存与攻击占用相同的攻击间隔:在进行攻击判定时…若无有效目标且能量储存数未满,则改为储存一份
攻击能量(属于攻击行为);仅没有有效目标,且能量储存数已满的情况下才进入待机状态", "攻击被打断且弹道未能成功生成的情况下,储存的
能量不会被消耗"; PRTS 异常效果 缴械 "秘术师储存能量同样无法进行".
- ai.js updateAlly: a ready unit (able to act, not disarmed) whose attack check finds no valid target — no target, or
its profile's canAttack false (a 秘术师 kit's target rule: 深靛's bound enemies, 维伊's marked ones) — calls the
profile's storeEnergy; a stored energy restarts the attack interval, a full store idles with the attack ready. A
DEFAULT cast at the check that leaves no target stores in the same check.
- professions.js mystic: storeEnergy (up to bb times), releaseEnergy (performAttack, after beforeAttack: a cancelled
shot keeps them) and hitsFn(b, u, info) — the energies leave with the attack and land with its main hit; the old
separate store timer (it started only once the attack was ready and never restarted the interval) is gone.
[ASSUMED] a redeployment holds no energy.
- 维伊 / 黑键 kits: their own tick stores become the kit's storeEnergy on the shared check; 深靛 / 维伊 no longer write
hadTarget themselves.
Golden: roster-003, roster-021, roster-022, bond-preciShip-high (深靛 stores at her check); matches solo-FUNNY-1,
solo-NORMAL-2, solo-HARD-2, solo-ABYSS-1, solo-ABYSS-2, coop2-NORMAL-3, coop2-HARD-4, coop4-FUNNY-6, coop4-ABYSS-7,
coop2-NORMAL-8-serverrun, solo-HARD-9-boosted, coop2-ABYSS-10-boosted, coop2-NORMAL-14-standins (each fields 深靛);
diy-032 (维伊) and diy-114 (黑键): the first store after a redeployment comes at the next tick's attack check instead of
the end of the redeploy tick (with the old tick store added back as a fallback the diy family matches the old digests).
fields, standins unchanged.
Tests: test/sim/feedback5-mystic-store.test.js (new, 4); op_veen, op_ebnhlz, kits_t1t2, kits_alt_t1 pass unchanged.
Docs: SIM.md §Professions (mystic row, profile hooks).
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.
- The S2 ward ("期间持续使自身与该目标周围的敌人陷入沉睡") re-applies a 0.5 s sleep every 0.25 s, so an enemy held asleep
was one entry for garrison_125 (「每当范围内有敌人或干员进入沉睡或晕眩时」 萨尔贡 / 精准 +1, elite +2, at most 24 / 48 per
battle). The reporter's first-hand memory of the official mode: 「攻击范围内有陷入沉睡就开始迅速增加直至上限」.
- The pulse now passes applyStatus `reenter` (a new opt-in: statusApplied reports `entered`; the buff refreshes as
before) — once per enemy and pulse, not while a longer sleep from elsewhere holds it. With one enemy in her range the
trait reaches its cap 5.63 s into S2; an enemy outside her range still adds nothing; a refresh elsewhere is still no
entry (the garrison rule is unchanged). No enemy wakes between pulses. The pulse timing is [ASSUMED] (not in the data).
- Tests: garrisons_battle — new 缇缇 125 + S2 test (normal and elite: an enemy at her back adds nothing over 8 s of
pulses; an enemy in range beside both wards, one entry per pulse, takes the trait to 24 / 48 within 6.5 s).
- Docs: DESIGN §24.8 (and the §24 intro), the Battle.applyStatus / garrison / kit comments.
- Golden: roster-042 (缇缇 S2): her trait's 24 layers come within her first S2 (26–37 s) instead of over four casts up
to 177 s; same totals (capped), and the earlier layers end the fight at 166.97 s instead of 182.03 s, 5 knock-outs
instead of 7.
- PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 ※: 「该付与来源获得所有标记单位的基础攻击力(最终加算)和阻挡数」. New
additive mod key atkFinal (PRTS 游戏数据基础 A_f = F_t[(A + D_p)(1 + D_t) + F_p]):
ATK = ((base + ΣatkFlat) × (1 + ΣatkPct) + ΣatkFinal) × ΠatkMul. The devour buff carries atkFinal instead of atkFlat:
1000 base ATK, +100 % skill, +2000 devoured → 4000 (was 6000). Every other mod key keeps its meaning.
- 「标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果」 cancels only the knocked-out
target's marks: a marker knocked out earlier in the pass now resolves its own marks, the kill credited to it
(「击杀来源始终为对应标记的付与来源」). A → B → C with B devoured first: C takes B's 5000 and the kill is B's. The 3
revive slots, the 联防 front-tile devour and the down-at-start rules (DESIGN §24.3 / §24.4) are unchanged.
- Point 5 (the 流失's own source is the target itself) stays open: the hooks still see the marker (DESIGN §24.7).
- Tests: bonds_core — new 最终加算 test (4000; a 直接加算 still scaled; ×0.5 scales the whole), new A → B → C test,
the dead-marker test rewritten (its mark now lands); feedback3-egir-down, feedback4-egir-unite, feedback3-ulpia-move
read atkFinal; match/bosshp recomputes ATK with the 最终加算.
- Docs: DESIGN §24.7 (and the §24 intro, §5.2 / §5.3, the §24.4 atkFlat lines marked), SIM.md §3, research 02 §3.6
and its JSON spec; data/bonds.json regenerated with build-data --offline — only egirShip.spec.algorithm[2] / [3]
changed.
- Golden: bond-egirShip-high (the devourers 深巡 / 幽灵鲨 / 斯卡蒂 no longer scale the devoured ATK by their ATK +%; the
fight ends at 89.03 s instead of 74.03 s), coop2-ABYSS-10-boosted (from round 8 ai_0 has 3 more 阿戈尔 and 奥术
layers — 海霓's first-kill trait — and both bots' boss damage moves). The marker rule moves no scenario.