feedback6: DESIGN §26.2–§26.13 assembled from the streams' drafts (联防 battlefield, 自选 picks in bond lists, consumables on a full carrier, Touch, 凋亡, 贾维, .gitignore, flyer hover, enemy animation states, runner messages, 频次 / leader summons, the collapse arrow), the §26 intro and the owner's decisions

This commit is contained in:
sganggs committed 2026-10-07 18:22:08 +08:00
1 parent a9fe794c3f
commit cfc149e277
1 file changed
+385 -2
+385 -2
View File
@@ -7,11 +7,18 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global).
0.2.1 is built on `feedback6`, branched from the 0.2.0 release (tag v0.2.0) and marked `0.2.1-dev` until it ships. Its
workstreams branch off it as `fb6-*`, each merged after review with its golden scenarios named. Each rule was checked
against the official data and PRTS; anything no source backs is marked [ASSUMED]. Where each is handled: the 突袭
landing on a teammate's half of a shared field → §26.1.
landing on a teammate's half of a shared field → §26.1; 联防 back on the round's battlefield → §26.2; the 自选 picks in
the bond lists → §26.3; a consumable on a full carrier → §26.4; the 外勤医疗 Touch → §26.5; 凋亡 and stored charges →
§26.6; 贾维's 拉普兰德 → §26.7; .gitignore → §26.8; enemy flyers over raised tiles → §26.9; enemy animation states →
§26.10; battle messages while the browser prepares a battle → §26.11; the 频次 器物 and leader summons → §26.12; the
bond strip's collapse arrow → §26.13.
Decisions of the owner: 2026-10-07 — a 突袭 member may land on a teammate's half when both halves of the field are
taken (the two-helper 联防 field, the Final Assault / Hidden Core pair field), replacing part of §25.18.1; 乌尔比安's
S3 【移动】 keeps his own board (§26.1).
S3 【移动】 keeps his own board (§26.1); 联防 plays on the round's battlefield again, from the owner's knowledge of the
official mode and a tournament video (§26.2, withdrawing §25.6.4); 0.2.1 within hours of the report; every operator at
full potential (pending, the R21P stream); the 自持有 bonus, multi-language voices and the title particles are not in
0.2.1.
### 26.1 突袭 may land on a teammate's half when both halves are taken (the owner's decision of 2026-10-07) — `sim/battle/players.js onFieldBoard`, `content/bonds/addon/battle.js raidTile`
@@ -44,3 +51,379 @@ S3 【移动】 keeps his own board (§26.1).
two landings stay on their own half). Tests: `test/sim/feedback5-raid-own-board.test.js` (4 cases; the pair and the
two-helper cases fail on the parent; the solo boss field's hand rows, the lone helper and 乌尔比安 beside a teammate are
unchanged). Docs: docs/SIM.md (`onFieldBoard`; `onOwnBoard` is now 乌尔比安's only).
### 26.2 「联防阶段地形全消失了」 (GitHub #244, reported by alien-zzy, official footage pointed out by RushFTK): 联防 plays on the round's battlefield again — §25.6.4 withdrawn — `match/unitePhase.js _uniteOpts`, `unite.js`, `screens/game.js`, `public/dev/game-mock.js`, `tools/golden.mjs`
- **Seen**: from 0.2.0 on, every 联防 was an empty two-part road — no walls, fences, water, crates, 活性源石, blowers or
band medic. Walkers went straight down row 9 where the round's battlefield walls them off (战场#01 cols 5–7), and the
helpers' 机变 terrain cards and the stage's devices did nothing there. In 0.1.x the 联防 was fought on the round's map.
- **Official**: the owner's first-hand knowledge of the official mode — 「官服保留地形,改回去」 (2026-10-07) — matching the
tournament footage RushFTK linked in #244 (舟戍杯#2: a 联防 on the round's map). **Why the level files misled us**:
act2autochess constData `escapedBattleTemplateMapSinglePlayer` / `MultiPlayer` name `level_act1autochess_escaped_single`
/ `_multi`, and §25.6.4 read that level's map as the 联防 map. But that map is the placeholder grid every wave
*template* level carries: the 39 template levels of the season (the round templates `01…07` and `h01…h08` with their
`_s` solo variants, the training `tr01/02/04`, both escaped levels) hold the identical 19 × 21 road grid, tile for
tile, while only the 11 stage levels (`m01…m07`, act2 `m01…m04`) have terrain and devices. Nobody plays a normal round
on its template's road: a template gives a battle its waves, routes and BGM (the 联防 BGM, PR #110, stays), the
stage gives the map.
- **Now**: `_uniteOpts` fields the 联防 battle on `this.stageId` in both combat modes, as 0.1.4 did: the match stage
opened to both halves (`GEO.UNITE_RECT`, cols 0–20) with its terrain, crates, water, devices, runes and band map
characters; the escaped templates' routes (one helper: `escaped_single` from the middle gate (9,10); two:
`escaped_multi` from col 18 through (9,10)); each helper's pieces on their prep tiles, the first of two shifted 8
columns onto the right half. `unite.js uniteStageId` is removed. The client draws the 联防 on the board's stage again
(`screens/game.js`: 0.1.4's `ownView ? ownStage : baseStage` — no map switch, nothing to restore on leaving; the
helpers see their own 机变 terrain changes, as in 0.1.4); the dev mock's 联防 likewise. Kept from 0.2.0: 突袭 and
乌尔比安's S3 land only on their own board (`Battle.onOwnBoard`; the raidTile change of another stream is independent
of this one), the payee of a bounty death no operator caused (the helper of that half — 战场#04's 活性源石 burns leaked
bounty enemies in 联防 again, §25.14.3), the 联防 BGM, the official result box at SETTLE, and everything else since
0.1.4. `data/stages.json` keeps the two `kind: 'unite'` records, which no field uses: they are the official level
data, the build checks them against `config.unite.templates`, and the engine tests of the own-board rules use them as
a plain two-halves road (乌尔比安 S3, 突袭 landing, the CB4 pins); dropping them changes no game behaviour and would
rewrite those tests (one of them belongs to the raidTile stream). `tools/build-data.mjs`: comments only — `node
tools/build-data.mjs --offline` reproduces all 15 data/*.json files byte for byte.
- **Checked against 0.1.4**: the 联防 battle input a match builds — stage id, rect, wave id, routes, spawns, helpers'
offsets, units and carried state, and the battle made from it (rows, ground / crate grid, device units, special terrain,
runes, map characters, ally tiles) — is byte-identical to v0.1.4's in 20 cases (战场#01, #04, #05(下半), #07(下半),
#08(下半) × 1 / 2 helpers × both combat modes).
- **GitHub #41 item 3** (an enemy pushed back across the spawn onto the other half): back exactly as in 0.1.4 — 野鬃 S2
beside the middle gate pushes 3–4 of `escaped_single`'s 20 walkers onto the right half (x ≤ 13.2), none off the map,
the same numbers on v0.1.4; they walk back through the left half and leak or die. The reporter accepted it 「不影响正常
运行就可以了」. The 0.2.0 road showed the same push (its right half is road too): §25.6.4 never removed it.
- [ASSUMED]: none new (the official 联防 field is the owner's knowledge, not a data reading).
- Golden: fields `unite-1` (escaped_single → 战场#02, act1autochess_m02) and `unite-2` (escaped_multi → 战场#07(上半),
act1autochess_m07) — the stages they had in 0.1.4; the matches whose 联防 rounds came out differently on the round's
map: coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins
(now a victory: walls and crates send the leaked walkers round through the helpers' range). coop4-FUNNY-6,
coop2-ABYSS-10-boosted and coop2-NORMAL-70-diy play 联防 rounds too, with the same digests on both fields. roster,
bonds, standins, diy unchanged.
- Tests: `test/match/feedback5-unite-map.test.js` rewritten — 6 tests: one / two helpers × both combat modes (the match
stage, its rows and walls, 战场#01's six crates on both halves, the helpers on their 围墙 prep tiles with the +8
columns, walkers going round the walls, the stage the leaker's spec / a watcher's m.field names), 战场#04's 活性源石 on
the field, and the same field with or without the escaped records — all 6 fail on the parent, and with soft asserts
every new assertion does. `test/e2e/coop.e2e.mjs`: the 联防 round's field meta and drawn stage are the match stage.
`test/data.test.js` (the records' test retitled: never the 联防 field). Docs: DATA §12, META §4, PLAYING §5,
design/geometry-time.md, I18N, the §25.6.4 pointer.
### 26.3 The 自选 picks in the bond popup's member list (the owner's report of 2026-10-07) — `ui/gameLogic/bonds.js bondMembers`, `ui/bondStrip.js BondPopup`, `ui/detailPanel.js resolveDetail`
- **Seen**: the owner's report of 2026-10-07 「自选编队的干员在局内对应盟约展开的名单里没有显示出来」. Reproduced with a
unit test of `bondMembers` (m.private.diy holding 推进之王 and 煌) and the popup in headless Chrome: the popup listed
every regular member — dimmed while not owned — but a 自选 pick only once a copy was owned (on the board, in the hand
or the 临时整备区), so a pick not bought yet, or not offered yet (the tier-5 slots from 调度中心 level 5, the tier-6
from 6), never appeared. Owned pieces did show in every case checked (board, hand, 临时整备区; normal and elite; 煌 in
both 炎 and 维多利亚; beside a 补位 stand-in of the same bond), with two smaller faults: an elite on the board drew its
row with the elite record (gold frame and golden avatar, unlike a member row, which always draws the base chess),
and in a watched teammate's popup a tap on their 自选 row opened the empty 甄选干员 slot's card (resolveDetail read no
pick for another player's member card).
- **Official**: neither the excel nor PRTS describes the popup's 自选 rows (PRTS 卫戍协议:盟约 only notes 「禁用盟约有可能
影响自选的支援干员」). The rule follows the owner's report: a 自选 pick belongs to the bonds of its operator like any
member.
- **Now**: `bondMembers` adds each pick of m.private.diy (the V and VI slots) to every bond its operator carries (the
composed record through the own getter, gameLogic/diy.js `diyGetter`): `owned: false` until a copy is on the board, in
the hand or in the 临时整备区 (then on board / in hand as before), `banned` when its slot is in m.private.diyBanned
(every bond of the operator off this match: it never shows in the shop). One row per slot, drawn as its normal form,
with the operator's name and portrait and the 「自选」 tag; a banned pick's tag sits on the portrait's bottom corner,
clear of the ✕ and the tier chip. The order rule is kept (on board, owned, tier, id: an unbought tier-5 pick comes
after the unowned tier-5 members). The 成员 header (`memberHeadCount`: the board, plus the hand for a bond that counts
it) and 在场 (the server's count) mean what they did — an unowned pick counts for nothing; the header's total includes
the picks, as it includes the unowned members. A tap opens the operator's card (the own pick, from m.private.diy).
A watched teammate: m.public sends no picks, so their popup still lists only their 自选 pieces on the field on screen
(UnitInfo `diy`, ui/watchBonds.js `ownerBoard`, as in 0.2.0); such a row now passes the unit's pick on (BondPopup
`onMember`'s fourth argument → the detail target's `diy` → `resolveDetail` composes their operator). The briefing and
the 本局信息 bond lists show no members (bond discs, banned-member counts, the 本局禁用干员 grid and the diyBanned
line), so nothing changes there.
- [ASSUMED]: how an unbought pick looks (dimmed, like an unowned member), how a pick whose bonds are all off looks (the
banned member's ✕), and the normal-form portrait for an elite copy (one row per member).
- Golden: unchanged (client only). Tests: `test/ui/feedback6-diy-bond-members.test.js` (5 cases, each failing on the
parent: the unowned pick listed and dimmed, header 0 / n + 2, its card; owned → on through the hand, the 临时整备区,
the board and the elite, one normal-form row; 煌 in 炎 and 维多利亚 beside 风笛's stand-in; the diyBanned pick; the
teammate's row opening their 煌, not the slot); `test/ui/watch-bonds.test.js` (the game screen's onMember wiring, now
with the pick).
Optional PLAYING.md sentence for the integrator (the 自选编队 paragraph, after 「和普通棋子一样计入盟约人数」 — not edited
here to avoid a conflict on that one-line paragraph): 盟约详情的「成员」一栏也列出你的自选干员(带「自选」标记;还没买到、商店
里还刷不出来时和没有的成员一样显示为灰色)。
### 26.4 A consume-on-equip item on a full carrier replaces first (GitHub #263, PR #264) — `match/player/items.js equip / _takeReplaced`, `ui/gameLogic/placement.js equipReplaces`, `match/bot.js itemTarget`
- **Seen**: with both equipment slots of an operator used, a normal 博士投影 dropped on it opened no replace dialog and
silently destroyed the OLDEST equipped item: the client's `itemAttaches` treated every `consume_on_equip*` item as
slot-free (`dropIntent` → `confirmReplace: false`), and the server ran the effect, whose `ev.keep` re-attached the item
through `_attach(holder, item)` with no `replaceUid` — the full row pushed out index 0 (a `replaceUid` sent anyway was
ignored). 盟约之币 and the other immediate consumables (随身身份牌, 紧急调度券, 精打细算玩偶, 简易通讯机, 见钱眼开玩偶, 骑士储蓄罐,
寻呼模块, 信标, 拟态物质, 人事部文档, the golden 博士投影) opened no dialog either and kept both equipped items
(reproduced on feedback6 783bd30: `scratchpad/w6-r21d-notes/repro.mjs`; reported by @SpiritedAwayCN with PR #264).
- **Official**: PRTS 卫戍协议/帮助 §装备 (fetched 2026-10-07): 「每名干员最多可佩戴2件装备」, 「达到上限强行佩戴会改为替换装备:
指定一件已佩戴装备替换为将要佩戴的装备,并销毁被指定的装备(可取消以中断替换操作)」 — no exception for the items the data
marks 装备时销毁; research 04's "never take a slot" is a [DATA] reading of `kind`, not a waiver of the dialog. The
reporter's first-hand account of the official mode: the picked item is destroyed first, then the item resolves, and the
operator is left with a free slot.
- **Now**: server — `PlayerState.equip`'s consume path takes the item `replaceUid` names (else the oldest, as for every
third item) off a full carrier before `onEquip` runs (`_takeReplaced`), so the effect sees the carrier without it (its
bonds: 随身身份牌 / 简易通讯机 / 寻呼模块 / 拟态物质); once the effect went through the item is destroyed (`onDestroy`
reason `replace`) and the slot stays free — the normal 博士投影 takes it (`ev.keep`) and no longer pushes another item
out. A refusal (博士投影 of either quality on an elite: `BAD_TARGET 'already elite'`) puts the item back at its index:
nothing is destroyed, the 博士投影 stays in the hand. 信标 destroys its carrier as before: the remaining item returns to
the hand, the picked one is gone. Client — `equipReplaces(ctx, uid, target)` (gameLogic) opens the replace dialog for
every item on a full carrier, a consumable too, except where nothing would be replaced: an attaching item that
completes an item merge (`equipMerges`; the server's consume path never merges) and a 博士投影 (its promoting effect
keys) on an elite. `itemAttaches` is gone. Bots — a consumable whose effect is the same on anyone (funds, 紧急调度券,
人事部文档 …) goes to a carrier with a free slot first (`itemTarget`, PR #264); the targeted ones (博士投影, 随身身份牌,
简易通讯机, 寻呼模块, 拟态物质, 信标) keep their carrier and, when it is full, replace its oldest item (bots send no
`replaceUid`).
- [ASSUMED]: the order inside the server is "take off → effect → destroy" rather than "destroy → effect": the effect sees
the carrier without the item (as officially), but the replaced item's `onDestroy` runs after the effect so a refusal
can restore it — no equipment item has an `onDestroy` effect for `replace`, so nothing observable differs.
- Golden: `matches` only, 10 of 18 move — in each the first point it parts from the parent is a bot's (or the AI 托管
human seat's) targeted consumable resolving on a full carrier, which now destroys the carrier's oldest item (traced
equip by equip against the parent: `scratchpad/w6-r21d-notes/trace-equips.mjs`): solo-FUNNY-1 (r9 ai_0 随身身份牌 →
突袭手雷 destroyed; bossPoolLeft 88535 → 91555), solo-FUNNY-2 (r9 ai_0 寻呼模块; bossPoolLeft 87617 → 107027),
solo-HARD-2 (r9 ai_0 随身身份牌, r12 简易通讯机; teamLp 17 → 13), coop2-NORMAL-3 (r9 ai_1 随身身份牌; teamLp 46 → 39),
coop3-ABYSS-5 (r11 ai_1 随身身份牌; ai_1's numbers only), coop4-FUNNY-6 (r4 ai_1 随身身份牌), solo-HARD-9-boosted
(r14 ai_0 随身身份牌), coop2-ABYSS-10-boosted (r13 ai_0 随身身份牌, r14 ai_1 简易通讯机; teamLp 398 → 529),
coop2-NORMAL-14-standins (r9 ai_0 随身身份牌; the match now won: 13 → 14 rounds, teamLp 0 → 12),
coop2-NORMAL-70-diy (r14 h_0 随身身份牌; numbers only). The bots' free-slot choice for target-agnostic consumables
moves no scenario by itself (six unmoved matches carry such a changed carrier: the effect is the same on anyone).
- Tests: `test/match/merge.test.js` (a full carrier with the picked item: normal 博士投影 takes the pick's slot, 盟约之币
pays and leaves a free slot, 随身身份牌 gives the carrier's bonds +3, the golden 博士投影 promotes and the other item
stays; 博士投影 of both qualities on an elite is refused with every pick and both items kept), `test/ui/leftovers.test.js`
(all 24 consumables open the dialog on a full operator and not on a free one, 博士投影 on an elite does not, an operator
in the hand the same; the real engine end to end: the dialog asks for 博士投影 and 盟约之币 and the picked item is the one
destroyed), `test/ui/gameLogic.test.js` (the consumable intent flipped), `test/match/bot-smarter.test.js` (盟约之币 to a
carrier with a free slot) — each new assertion fails on the parent; the dialog e2e `test/ui/leftovers.e2e.test.js`.
### 26.5 The 外勤医疗 Touch casts on its 5-2, and the map characters' card is an operator card (GitHub #260, PR #278) — `content/tokens.js touchKit`, `ui/detailPanel.js MapCharDetail / SKILL_TEXT_NOTES / skillTags`
- **Seen** (reported by @CurlyZhu on 0.1.4; PR #278 by @IceCodeNew): the 外勤医疗 strategy's Touch and 预备干员-医疗 showed as
「召唤物」 with base stats only — no skill, no talents. Touch waited for an injured ally inside her own 3-3 before casting
恳切福音 although the skill heals on its 5-2, so a 雷蛇 losing HP to 源石溶剂 five tiles ahead was never healed.
Cause: tools/build-data.mjs builds the map characters as tokens (kind `mapChar`, `variants: {}`; stats, skill, talents
and 特性 on the record) and `resolveTrigger` widens operators' skills only, so their records keep DEFAULT; TokenDetail
read skill and talents from an owner variant only (none) and always printed 召唤物.
- **Official**: PRTS Touch(卫戍协议) — an operator of the mode (医疗 · 医师, 远程位), talents 攫升 / 超脱, S3 恳切福音 手动
触发, 技能范围 5-2, 「攻击距离+2」 corrected by PRTS to 攻击范围扩大, 「不高于一半」 corrected to 低于 (原因 6);
aceffect_band_61: a 预备干员-医疗 appears, replaced by Touch with two elites on the field. The larger-range cast is the
owner's rule of 2026-10-05 (§25.5), which the Touch 补位 already runs by its data (backups.json S3 `ACTIVE_RANGE` on the
5-2).
- **Now**: `touchKit` sets the skill's trigger to `ACTIVE_RANGE` on the skill's own range (the running range, which
strictly contains her 3-3): an injured, healable ally inside it casts; a full-HP or 禁疗 one does not (the reserve
medic's 治疗强化·β型 has no range of its own and stays DEFAULT). The card (`MapCharDetail`, taken for `kind: 'mapChar'`):
chip 干员, the class line (医疗 icon, 远程位), the 特性, the four stats (live in battle), the skill row of the operator
card (icon, SP type, trigger, initial · cost, duration — `skillTags`, shared with ChessDetail) with its text, and the
talents — all from the record, translated by the data overlays. Checked against the kit: 恳切福音's stored sentence
matches what the sim does (ATK +40 %, ×150 %, 2 targets, the 30 % extra heal; the 5-2 = her 3-3 two tiles longer)
except 「不高于一半」 — touchGospel boosts strictly below half (PRTS 修正) — so the card keeps the official sentence and
adds a line under it (`SKILL_TEXT_NOTES`, by skill id; on the Touch 补位's S3 card too, whose text reads the same).
The triage's "+40 %" doubt does not hold: bb.atk of the map character's Lv10 skill is 0.4, which the kit applies.
Enemy targeting is unchanged (the units stay `kind: 'token'` in the sim; no enemy that skips them was named).
- [ASSUMED]: none new (the trigger is the standing owner rule).
- Golden: roster-041 (its strategy Touch: heal 8438 → 17072, heal actions 19 → 27, casts 2 — the same numbers as PR #278;
p1 deaths 7 → 4) and matches coop2-NORMAL-14-standins (h_0 plays 外勤医疗: defeat in round 13 → victory, team LP 16) —
the two battles with that strategy's Touch; nothing else moved.
- Tests: `test/content/tokens_devices.test.js` (Touch on her stage slot, an ally only in the 5-2: no cast at full HP, none
while 禁疗, then a cast and a heal — fails on the parent), `test/ui/playtest6_summons.test.js` (both cards: 干员, 医疗,
skill / talents / 特性 text, the 低于 line only under 恳切福音, a summon keeps 召唤物 — fails on the parent). i18n: one new
string in en / ja / ko / zh-TW. Docs: docs/SIM.md (ACTIVE_RANGE also set by touchKit).
### 26.6 An operator's 凋亡 drains the stored charges too (PR #262; #261 its closed duplicate, both by @siruimei07) — `sim/damage.js resolveBurst` (apoptosis, ally side)
- **Seen**: the burst's −1 SP/s took only `sk.sp`, the SP towards the next charge. A full skill keeps `sp = cost` with
every charge stored, so it kept all of them; a one-charge skill stayed ready through the whole 15 s (and cast the moment
静默 ended). Reproduced with 精锐 雪猎 (S2 可充能2次, 14 SP): two charges stored before the burst, two after it.
- **Official**: PRTS 元素 — 凋亡损伤 on a 我方单位: 「立即获得等时长的阻回、静默,该阻回、静默持续期间每秒损失1点技力并受到100点无来源
法术持续伤害」 (15 s); PRTS 技能 §特殊属性 可充能: 「当前技力上限等于该技能技力需求的X倍」 and 「当持有者的技力流失时,充能次数也
会实时降低(但因达到上限产生的额外效果会依然生效)」.
- **Now**: each second `sk.setSpTotal(max(0, spTotal − 1))` — the official 技力 (charges × cost + the partial bar),
charges rebuilt silently (no spGain hook, 阻回 untouched); a passive, a skill-less unit, a free skill (spTotal 0) and a
running duration / ammo / toggle skill are left alone as before (setSpTotal refuses those too). An effect a full charge
produced is not undone (none is modelled on this path).
- [ASSUMED]: none.
- Golden: 5 moved, each by a burst on an operator with SP stored — 假想敌:管's 凋亡 strikes and the 萨卡兹王庭军's 凋亡 on
hit: fields hidden-boss_10-solo (泥岩 loses its stored charge, casts 1 → 0 — the move PR #262 reported; team heals 73194
→ 72505), boss-boss_3-solo, hidden-boss_10-pair and diy-058 (later snapshots and event counts only, same results),
diy-059 (黍 forms: casts 11 → 5, 6 → 2, 9 → 5, 6 → 1; kills 39 → 47, leaks 57 → 49).
- Tests: `test/content/feedback6-apoptosis-sp.test.js` (2: 雪猎's two charges → one after 1 s, none after 15 s; a full
one-charge skill not ready after 1 s, −15 技力 in all, no spGain, a running timed skill unchanged — both fail on the
parent); `test/sim/combat.test.js` operator bursts unchanged. Docs: docs/SIM.md element table.
### 26.7 A 拉普兰德 贾维 grants during a refresh waits for the next one (PR #196 by @SpiritedAwayCN, the part 0.2.0 lacks) — `match/effectsMeta.js dispatch / _garrisons / refreshPieces`
- **Seen**: 贾维 【团伙行动】 grants a 叙拉古 operator from the dispatch of every 6th manual refresh (band step 2), before the
garrisons (step 4), which walked the board and hand live — so a 拉普兰德 granted (or the elite her copy completes) ran
her 刷新时 特质 in the refresh that granted her and spent her 「本回合首次主动刷新」 on it (+4 / +8 at once, nothing on the
next refresh).
- **Official**: garrison_123 「<刷新时>若为本回合首次主动刷新…此干员在整备区时也有效」; the players' reading (§21.1, after
0.1.0) 「获得该干员后该回合的首次刷新」 — a refresh she was gained in is not one she was there for. 贾维: 「主动刷新6次调度中
心后获得一名…<叙拉古>干员」.
- **Now**: `dispatch` takes the board (reading order) and hand chess before the first handler of an onRefresh; the
garrison step runs each of them that is still on the board or in the hand, where it stands now; a chess gained during
the dispatch does not run, nor one that left (sold, consumed by a merge, sent to the 临时整备区). Other hooks keep the
live walk. Not taken from PR #196: copying round counters on a merge and keying the counter by garrison id — 0.2.0
counts a merged elite as a new 拉普兰德 and keeps an in-place promotion's count (the owner's decision, §25.13.3).
- [ASSUMED]: none new.
- Golden: unchanged.
- Tests: `test/match/feedback1-meta.test.js` (2: a gifted copy and a gift that completes the elite — nothing in the 6th
refresh, +4 / +8 in the 7th, nothing in the 8th; both fail on the parent). Docs: docs/META.md (SERVER_REFRESH_SHOP row),
the effectsMeta.js header, garrisons/meta.js.
### 26.8 .gitignore lists .npm-cache/ (GitHub #258, reported by @qfmc7040) — `.gitignore`
- **Now**: a local npm cache (`npm_config_cache=./.npm-cache`, `npm ci --cache .npm-cache`) is ignored like node_modules/
and .cache/; the packages ship tracked files only, so it stays out of them too. Golden: unchanged. Test:
`test/gitignore.test.js` (the entry, and git's own check-ignore; fails on the parent).
### 26.9 An enemy flyer hovers from the road whatever tile it crosses (GitHub #277 by @FrogThai) — `render/units.js UnitView.sync / shadowZ`
- **Seen** (GitHub #277, 飞机经过一格方块时会跟走楼梯一样,有高低差): a flying enemy whose route crosses one high-ground or
forbidden block rose onto it and dropped back, like a step. Cause: `UnitView.sync` put every unit except a *ground*
enemy on the tile top under it (`groundZ`: TILE_H.wall 0.42, forbid 0.3) and added `FLY_HOVER` on top, so an enemy flyer
followed the tiles (reproduced headless, `w6-r21f-notes/flyer-277.mjs`: 妖怪 along row 9 of act2autochess_m01 over the
'#' at col 6 — body height 1.293 … 1.600 tiles on both boards before, 1.293 … 1.300 after).
- **Official** (docs/research/12, §25.13.5): the fly offset is one constant, `Vector3(0, 0.35, 0)` added while flying, ≈ 1.3
tiles over the route; nothing per tile. High ground is where units stand, not a step under a flyer.
- **Now**: every enemy keeps z 0 (ground enemies already did; a flyer hovers `FLY_HOVER` above the road); operators and
summons keep their tile top (an operator on high ground stands on it). The flyer's shadow lies on the tile under it
(`shadowZ`, eased like the body), so over a block it is drawn on the block top. The 2D and the 3D board share the
camera and the Pixi unit layer, so both are fixed.
- [ASSUMED]: the shadow on the block top under a flyer (no official reference for a shadow over a block).
- Golden: unchanged (client only). Tests: `test/render/unitview.test.js` `an enemy flyer crossing a raised tile keeps its
height (GitHub #277)` (2 cases; the body-height case fails on the parent: it rose 0.415 tiles). Headless screenshots
before / after, 2D and 3D: `w6-r21f-notes/{before,after}-{2d,3d}-{before,over,past}.png`. Docs: ASSETS (flying units),
design/client.md, research 12 §6.
### 26.10 Enemy animation states: per-slot skill clips, two stun spellings, the Run cycle, two 重生 (PR #275 by @xcdoge, client part) — `tools/assets/anim-roles.mjs`, `tools/assets/spine.mjs`, `render/spine.js _applyRoles`, `render/units.js setSkillSlot / moveFast / FORMS`, `render/app.js 'cast'`
- **Seen** (PR #275, a scan of every enemy skeleton against its manifest roles): (1) an enemy's manifest only had skill
index 0 (tools/assets/plan.mjs passes [0]), so a multi-skill boss could show one cast clip — 盐风主教昆图斯 has
Skill_01..04; (2) the stun resolver knew Stun / Stun_Begin / Stun_End only, so 巨大的丑东西 (Stun_1 / Stun_2) and 斩胄之剑 /
破胄之锤 (Dizzy_*) had `stun: null` and froze their clip; (3) 猎狗pro walked on Move_Loop (0.80 s) though its skeleton has
Run_Loop (0.53 s) and it is fast (moveSpeed 1.9, 行动速度很快); (4) 巨大的丑东西 and 自在 have a Revive clip and a 重生
in the sim but no FORMS entry, so their 重生 played nothing.
- **Official**: clip names and lengths are the skeletons' own (data/assets.json `animations`): 巨大的丑东西 Revive 8.67 s for
its 10 s `reborn.duration` (its self-destruct 2.17 s in), 自在 Revive_01 5.33 s for 5 s. The sim reports a 重生 as a stun
(archetypes.js `reborn`: flags.stun), so the 重生 form must say what the rest of it holds.
- **Now**: the resolver resolves every numbered skill clip of an **enemy** after the caller's indices (`numberedSkills`,
passed by spine.mjs for enemy models; the first index stays the primary `skill`; operators keep the pool's indices, 宴's
PREFAB_SPINE_ROLES fix of §25.22.9 unchanged); the stun loop is Stun → Stun_1 → Dizzy_Loop (with *_Begin / *_End); a
model's own Run cycle is a separate `run` role. The renderer: `SpineActor._applyRoles` composes the skill slot's clip,
the Run cycle and the form in force (a slot change keeps a form; a form ending keeps the slot); a UnitView of an enemy
faster than moveSpeed 1 moves on `run` (猎狗pro 1.9, 深池侦察犬 / pro 1.7; 宿主士兵 0.9 and 鸭爵 0.4 keep Move);
`UnitView.setSkillSlot` (render/app.js `case 'cast'`) swaps an enemy's skill clip. FORMS: 巨大的丑东西 `reborn` = Revive
as the change clip, then the 大祭司's Idle_2 held (also as the 重生's stun) → `form2` Idle_2 / Move_2 / Die / Stun_2, no
attack (不进行攻击); 自在 `reborn` = Revive_01 → `form2` its own clips. data/assets.json regenerated with
`node tools/fetch-assets.mjs --offline`: 18 enemies gain `anims.skills`, 6 `anims.run`, 3 a stun clip instead of null
(巨大的丑东西, 斩胄之剑, 破胄之锤 — the two 胄 parts are stun-immune, so only 巨大的丑东西's is ever seen); no operator or token
entry changed (`w6-r21f-notes/manifest-diff.txt`).
- **Not ported**: the sim side of (1) — `content/enemies.js castSkillSlot` emitting `['cast', id, slot]` when an ability
casts, 昆图斯's ability ids, `UnitInfo.skillIndex` for enemies — adds an event, so golden's event counts move (+cast in
the two 昆图斯 fields); it is left for the owner (patch: `w6-r21f-notes/sim-cast-slot.patch`, not applied). Until then the
per-slot clips are unused and every boss keeps its primary clip. Note: 昆图斯 has 10 data skills (Tidewater … Doom) and 4
clips, so mapping the G1 / G2 / Doom slots to Skill_04 or the primary needs a check first. The PR's research note
(docs/research/13-animation-states.md) is not ported.
- [ASSUMED]: the Run threshold (moveSpeed > 1; the client's own trigger is unknown) — only a model with a Run cycle changes;
巨大的丑东西's `_1` / `_2` clips as its first form / the 大祭司 (the 大祭司 set has no attack clip, matching 不进行攻击); 自在's
second form on its first-form clips.
- Golden: unchanged (client and asset tools only). Tests: `test/assets.test.js` (resolver: numbered enemy skills, Stun_N /
Dizzy_*, run role; the manifest: every stun / Run clip has its role, every enemy with numbered skill clips has a clip per
slot, 宴 unchanged — 6 cases, all fail on the parent), `test/render/forms.test.js` (the two 重生; every clip FORMS names
exists in its manifest entry), `test/render/loadout-skill.test.js` (an enemy cast slot swaps the clip, a form kept),
`test/render/unitview.test.js` (moveSpeed 1.9 on Run, 1 on Move, composing with the slot);
`RENDER_E2E=1 node --test --test-concurrency=1 test/render/models.browser.test.js` 7/7. Docs: ASSETS (`Roles`: skills of
an enemy, run, stun spellings).
### 26.11 A b.end, a second b.start, b.pool and the pause reach a battle the browser is still preparing (PR #266 by @siruimei07) — `public/js/battle/runner.js` (`pending`, `prepare`, `onEnd` / `endBattle`, `onPool`, `setPaused`, `clear`)
- **Seen** (PR #266; reproduced on feedback6 783bd30 by `test/match/runner-pending.test.js` — 11 of its 15 cases fail
on the parent): the client runner added a `b.start`'s battle to its map only after the sim module had loaded
(`ensureSim`) and the new Battle had been caught up silently to the field clock (one animation frame per 600-tick
slice), and everything else looked only at that map. **Cause** (runner.js at 783bd30): `onEnd` (line 688,
`entries.get`) dropped a `b.end` that came during the load or the catch-up — after a takeover the browser went on
reporting as the authority (the server ignores a non-authority's reports, server/match/match/reports.js), and a forced
end (the Final Assault's `cleared` / `forced`) never ended the battle, so the server waited out its 6 s grace and
filled the field with the stats-only stand-in; `onStart` (line 595) built a second Battle for a second `b.start` of
the same battle during the catch-up; line 624 (`seq !== startSeq`) never built an authority that another field's
`b.start` superseded during the load (the deadline then took the field over); lines 663–666 re-registered an
authoritative battle whose catch-up the next prep's `clear()` had interrupted, and it went on running and reporting
into the next round; `setPaused` (line 506) and `onPool` (line 680) skipped a battle in catch-up (the paused time was
caught up; the boss pool waited for the next `b.pool`). No wrong settlement came of it: the server re-simulates or
stands in.
- **Official**: not a game rule — the remake's own client / server protocol (§14); nothing to check in the game data.
- **Now** (§14 Client runner, `b.end`): a `b.start` is registered as pending the moment it arrives (`pending`,
`battle` null until built; `prepare()` loads, builds, catches up and registers it), so what comes for that battle
meanwhile is applied to it in arrival order: `b.end takeover` clears its authority; a forced / timeout `b.end` keeps
the first end reason and ends the Battle as soon as it exists — before any catch-up, or at once mid-catch-up — and an
authority reports it then; a second `b.start` of the same battle updates the same entry (authority, watch, the field
clock while it is not built yet) and never builds a second Battle; `b.pool` syncs a built pending boss Battle (one
not built yet takes the last pool when built, as before); the solo pause shifts a built pending battle's clock. A
`b.start` of another field supersedes it: an authority, or a battle the server already ended, is still built and
registered (a superseded authority catches up in the background with the bounded fast-forward, as before), a display
replica is dropped. `clear()` (the next prep, the match over, leaving) drops the pending entries; their preparation
finds itself gone at its next step and builds, steps, registers and reports nothing — a fresh `b.start` of the same
battleId afterwards is a new preparation the old one cannot touch.
- [ASSUMED] A forced / timeout end that arrives before the Battle exists ends it where it stands (tick 0, or wherever
the catch-up was) instead of catching up to the field clock first (as the PR does): the server sends those ends only
when the Final Assault / Hidden Core ends, and after a forced end it credits no further report (§20.5), so catching up
first would only cost time. A superseded display replica the server already ended is kept (registered, finished) so
switching back shows it ended (as the PR does).
- Not taken from the PR: its `cancelled` guards on a result request already in flight when the round moves on (a
timeout may still retry it once; the server ignores a stale battleId — not a lost control message) and its
cancellable frame wait (the preparation notices a clear or an end at its next frame). Its `docs/DESIGN.md` paragraph
became the §14 Client runner sentences (DESIGN.md is the index now).
- Golden: unchanged (client only). Tests: `test/match/runner-pending.test.js` (new, 15: a b.end takeover / forced /
takeover-then-forced, a second b.start, the next prep and another field's b.start, each during the load and during
the catch-up; a superseded replica, the pause and b.pool during the catch-up); the runner suites
`test/match/runner.test.js`, `clientCombat-review`, `feedback1-runner-forms`, `diy-shop`, `standin-ownership`,
`watch-bonds`, `test/ui/playtest6-unite.test.js`, `test/ui/spectate-effects.test.js` unchanged and green.
### 26.12 The round's enemy effects reach the 频次 器物 and the leaders' summons (PR #272, 感谢 @CXUtk) — `content/enemies/archetypes.js times`, `content/bosses.js summonMods`, config `enemyScale[r].supplyHp`
- **Seen**: a 器物 (the 14 hit-count keys: 瑞印, 卷轴, 铜镜, 镇纸, 茶器, 矛头, 灯盘 and their _2) needed the data's number
of hits at every difficulty: spawnEnemy multiplied its max HP by the round's hpMul, then the 频次 archetype's
`setHits(e, e.def.maxHp)` overwrote it with the raw count (reproduced: 青铜镜 30 hits in solo 标准 R1 — data × 0.75 —
and in co-op 终极 R15 — data × 4.30 — alike, as a 悬赏 card and as 身观's death spawn). A leader's mid-fight summons —
死亡集群's 妖怪, 刺胄之弹, “裂管之音”/“断弦之音”, 不祥幻影 — spawned with no round mods at all (800 HP / 8 hits / 10 hits /
10000 HP at every difficulty). Reported by @CXUtk in PR #272.
- **Official**: activity_table act2autochess (data/effects.json): the round effects are ENEMY effects on 所有敌人 —
攻坚装备 / II / III `enemy_attribute_mul` (ATK ×1.1 / 0.8 / 0.7, HP ×1.2 / 0.8 / 0.75) and 急行军 (speed +15 %) with
`enemy_exclude` = 炎佑 only; 补给线 / 补给线II (HP ×1.08 / ×1.2, no ATK) with `enemy_exclude` = 炎佑 + exactly the 14 器物
keys. The exclusion only makes sense if a max-HP multiplier changes a hit-count unit's count, so the 器物 take 攻坚装备's
HP and not 补给线's. The per-round table (PRTS 卫戍协议:盟约/下半 §开始模拟, user-provided; research 01 A3): `hp =
hpBase · 1.2^kHp · extra`, `atk = atkBase · 1.1^kAtk`; 攻坚装备 always brings ATK, so kAtk counts its stacks and the
HP-only steps 1.2^(kHp − kAtk) are 补给线II, `extra` 1.08 is 补给线 — only co-op 终极 has them (R5–R15). PRTS: "领袖单位于
服务器的生命值加成不受上述加成影响" — only the leader's HP (the server pool) is exempt. 炎佑 is a bond ally in this
remake (spawnYanyou), never an enemy (waves.js skips it in templates): its exclusion needed no change.
- **Now**: build-data writes `enemyScale[r].supplyHp` = 1.2^(kHp − kAtk) · extra when ≠ 1 (co-op 终极 R5 1.2, R6 1.08,
R7 1.44, R8–R11 1.728, R12 2.0736, R13 1.86624, R14–R15 1.5552; only config.json changed in the rebuild, checked by
a JSON compare); gamedata.baseEnemyScale returns it as `supplyHpMul` and the wave / 悬赏 mods carry it (waves.js
`roundMods`). The 频次 archetype sets hits = the spawn's max HP (data × hpMul) ÷ supplyHpMul, rounded (setHits); a 频次
enemy's death spawn inherits the parent's mods, so 身观's 青铜镜 counts the same. A boss / hidden field gets the round's
scale as the BattleSpec flag `flags.enemyScale` (bossRounds.js; golden fields and tools/balance.mjs likewise), and the
leaders' summons spawn with it (`summonMods`): HP / ATK / speed, 死亡集群's `summon.hp_ratio` multiplied on top; 刺胄之弹
and 余音 set their hit count from that HP like the 器物 (they are in no exclusion list). 王权号令's equipment
(invulnerable, untargetable, never fights) is left as it was. Examples: 青铜镜 solo 标准 23 hits, co-op 险境 R13 86,
co-op 终极 R6 62 (1.2⁴, not 1.2⁴ × 1.08), R14 129; co-op 终极 R14 刺胄之弹 53 hits, 余音 67, 妖怪 5350 HP.
- What the PR did and we did not take: it back-calculated 补给线 from the composed hpMul (recognising only a 1.08 / 1.2
ratio against 1.2^kHp), which counts the 补给线II steps as 攻坚装备 (co-op 终极 R5 / R7–R12 器物 too many hits), and kept
fractional HP (a ceiling); it also scaled the husk counts.
- [ASSUMED]: a fractional count rounds to the nearest whole hit (setHits' Math.round, kept; a ceiling — float HP — would
differ for 3 × 0.75 and 35 × 0.75); the summons' hit counts follow their HP (inferred from the 器物 exclusion, not
measured). Open: the 逐火 ember / 再生 傀儡 counts (talent `Revive[Trigger].prop_max_hp`) stay unscaled — whether the
effect multiplies a talent-set max HP is not in the data.
- Golden: 74 scenarios. 器物 counts (every one holds a 器物 — extra spawn, 频次 wave or a 频次 enemy's death spawn): roster
37 (002 003 004 005 007–020 022 023 025 026 029–034 036–041 043 045 049), bond-soloShip-high, diy 9 (036 040 056 062
068 073 091 114 115), boss-boss_6-solo (俗心's 小说卷轴), matches 16 (all but solo-ABYSS-2 and coop2-NORMAL-70-diy: solo-FUNNY-1/2,
solo-NORMAL-1/2, solo-HARD-1/2, solo-ABYSS-1, 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 — 频次
rounds / 悬赏 and the Final Assault summons). Summons: boss-boss_1-pair/solo and hidden-boss_8-pair/solo (妖怪 +
刺胄之弹), boss-boss_3-pair/solo and hidden-boss_10-pair/solo (余音), boss-boss_5-pair/solo (不祥幻影). standins unchanged.
- Tests: enemies_bosses — the 14 per-key 频次 tests (hits × the round's HP, was "must not change"), the data pin (the 14
kitTimes keys = 补给线 / 补给线II `enemy_exclude` minus 炎佑; 攻坚装备 / II / III / 急行军 only 炎佑), hpMul ÷ supplyHpMul with a
death spawn and the solo 标准 rounding, leader summons with / without `flags.enemyScale`; waves — supplyHpMul per co-op
终极 round (hp ÷ it = 1.2^kAtk), wave and 悬赏 mods; finalAssault — the boss fields carry `flags.enemyScale`.
### 26.13 The bond strip's collapse button is a bare arrow (the owner's request of 2026-10-07) — `screens/game.js`, `public/css/screens/game.css .bonds-toggle`
- **Seen**: the boxed 「收起」 / 「盟约」 tab in front of the bond discs looked heavy (0.1.4's fold button, PR #149, GitHub #142).
- **Now**: a bare chevron (‹ open, › folded), transparent, at 55 % opacity (80 % when folded), full opacity and mint on
hover / keyboard focus; the 44 × 44 touch target stays as an invisible hit area and the name is the aria-label
(展开盟约 / 收起盟约). Tests: `test/ui/bond-collapse.e2e.test.js` (SP_E2E, 2).