diff --git a/docs/history/0.2.1.md b/docs/history/0.2.1.md index affaf2d6..4bed37d8 100644 --- a/docs/history/0.2.1.md +++ b/docs/history/0.2.1.md @@ -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).