docs/SIM.md §2: the countdown (attackCountdown, 1e-9 like timeLeft / buffs / every()), 1 s = 30 ticks and 3 s = 90, the
双眼皮 turrets and 寒檀's icicles on it, the drones that compared with 1e-9 already, the count-up content clocks not
covered; the official note — PRTS 作战机制/sandbox 「帧对齐机制对攻速的影响」: one frame = 1/30 s, since 2019-12-24 a
non-integer frame count is rounded (四舍五入; before, 进一取整), an exact one stays; here a non-integer count still ends on
the tick that crosses 0 (⌈30 × interval⌉: 1.25 s = 38, 能天使's 0.678 s 21 against the official 20) — an older
difference, unchanged. §7.1: SP within 1e-9 of the cost is the cost (gainSp, _normalize). design/geometry-time.md §4:
the engine's timer tolerance in one place. history/0.2.2.md §27.61 (Seen / Official / Now / Golden with the end-result
flips one line each / Tests / Docs) and the area map's "→ §27.61". docs-consistency + docs-paths: 31 pass.
- The owner asked to verify the official rule before deciding (research: PRTS 失衡位移机制 / 推与拉 / 游戏数据基础 / 特殊机制,
the client's Rigidbody2D data); the PR's 0.6387·√tiles formula and fear exemption were not taken.
- Grok review: MERGE AFTER FIXES — two HIGH (static pulls, 弧光锋卫's bleed), skills during the hold, husk 重生, exact
frames — fixed on the branch (1db84d79).
- Golden union: bond-miraShip-low differs from HEAD (fewer enemy attacks: 169 → 166 events, same kills) though the
branch alone left it — HEAD's ×1.1 练度 path now displaces an enemy there. Broad node --test 5681 (5679 pass, 0 fail,
2 skipped); lint 0 errors.
Branch review of fb7-unbalance (MERGE AFTER FIXES):
1. A 静态刚体 pulled got the 0.1 s floor: _staticForce now knows push from pull — a push's force is instant (推与拉), so
0.1 s; a pull's lasts its window (pullUnbalance: 1 s, 0.5 s below −1) — PRTS 特殊机制 静态刚体 「在失去施力后立刻」 out,
「0.1 秒保底」. pull() checks the puller's own block before the static case.
2. 弧光锋卫 (PRTS 天赋 「处于失衡状态时,每0.066s受到400点无来源真实持续伤害」, data unbalanced_bleed): a tick bleed of 400 every
0.066 s while unbalancedNow holds; the per-tile one-shot from the position jump ([ASSUMED], UNBALANCE_SPEED) is gone.
A 受力等级 0 push (0.8 s) → 12 hits = 4800 (was 2037), a 急停 pull with no movement (1 s) → 15 = 6000 (was 0), a
wall-shortened slide keeps the 0.8 s row → 4800.
3. 失衡免疫 「失衡期间无法自主移动、发动攻击、使用技能」: enemies/helpers.js unbalancedNow; canCast(e, sil, battle) refuses while it
runs (content skills; the boss parts' calls pass the battle), blinkForward returns null, 枯朽之种's dive / blast and the
铳 / 碎铳之簧 charges wait, 乌顶巨角卢鲁's clash waits for its end. A route APPEAR relocation clears it (a forced state
switch, 失衡位移机制). Left running on purpose (docs/SIM.md): 水遁忍者's damage check (异常效果 「仍然能正常造成伤害」), passive
auras and terrain / periodic effects, death / damage-taken triggers, skill cooldowns, 弧光锋卫's bleed itself.
4. husk() (逐火 余烬 重生) clears the state like reborn().
5. The 35/30 row held 36 frames (t0 + 35/30 overshoots in floats): every check compares with 1e-9; the frames are asserted.
Tests: test/sim/unbalance.test.js +6 (exact frames 6 / 12 / 24 / 27 / 32 / 35 with the walk on the next frame; static pull
1 s / 0.5 s, push 0.1 s; 弧光锋卫 4800 / 6000 / 4800; canCast / blinkForward refused, 枯朽之种 held then diving; the elk's clash
after the state; APPEAR and a 逐火 重生 clear it — all six fail on the parent); enemies_bosses (失衡 kit test) and
feedback1d-solvent (BUFF damage) updated to the state bleed. test/sim + test/content + test/render pass. Docs: SIM.md
(push / pull row), PLAYING.md, design/engine.md (helpers, enemy AI), constants / displacement comments.
Golden against the previous commit — outcome moves: roster-010 (歌蕾蒂娅 pulls 妖怪 drones, now held 1 s: killed 44→45,
leaks 58→57), roster-040 (static pulls of 妖怪: killed 28→25, leaks 37→40, deaths 29→30), roster-043 / 044 (山: deaths 14→16 /
14→13), hidden-boss_9-pair (锏: boss HP 3636991→3586929), coop4-ABYSS-7 (ai_2 LP +1 from round 5), diy-115 (黑键: leaks
70→75), diy-116 (黑键: killed 2→3, leaks 121→120) — the static pulls (probe) and the 1e-9 end (a frame shorter where the float
sum overshot: the 35/30 row, some pull windows); other moves (damage / snapshots only): roster-008 / 017 / 039, 9 bonds,
boss-boss_4-pair / boss_6-pair / hidden-boss_10-pair, coop2-NORMAL-14-standins / coop2-NORMAL-3 / solo-ABYSS-1 /
solo-HARD-1 / solo-HARD-9-boosted / solo-NORMAL-1, diy-053 / 085 / 102.
- Conflicts (the branch started at ae22335c): the four i18n packs (union of keys), game.js DetailPanel props (ops + the
voices' voice=true), detailPanel.js resolveDetail (cultOpts + the voices' tap), match/views.js (both helpers),
docs/PLAYING.md and design/network.md merged line by line against the base. test/match/bosshp.test.js's ATK formula
now includes the unit's 练度 multiplier (cultMul).
- Grok review: MERGE AFTER FIXES — the note and the module card at the set potential fixed on the branch.
- Golden: every owned operator ×1.1 ATK / DEF / max HP by default — roster 49, bonds 46, fields 22, matches 18, diy 134
(stand-in battles and prototype picks unchanged). Broad node --test 5666 (0 fail after the bosshp formula; 2 skipped);
lint 0 errors; typecheck; i18n strict clean (1152 msgids).
#400 by @PCL1234: 干员调配 (and every card that shows the elite with REA-Y — 局内数值, the shop / detail card) read
「每攻击到一个敌人回复自身12生命」; the battle heals 50. Cause: official battle_equip_table uniequip_003_excu2 has a
DISPLAY part (display only) whose blackboard `value` 12 fills its own added line 「攻击范围内存在2名及以上敌人时攻击速度
+{value}」; tools/build-data.mjs applyModuleTraitParts merged that blackboard into the trait's before writing the class
trait 「…回复自身{value}生命…」, so the trait line took 12 (not potential, not the module level: the text template).
The sim already healed 50 (the kit reads traitBase for REA-Y). Now the trait line is written with the blackboards of
the parts that carry trait data (TRAIT / TRAIT_DATA_ONLY / OVERWRITE_BATTLE_DATA) or rewrite the line; a DISPLAY part
that only adds a line fills that line. `trait.bb` (what the kits read) is unchanged, as are tokens' traits.
- Data: chess.json — exactly chess_char_5_01_b REA-Y traitOverride desc / descRaw (12 → 50); backups / tokens and
every other file identical. data/i18n rebuilt offline (en / ja / ko / zh-TW): only that text and the record hash.
- Docs: DATA.md `trait` row.
- Tests: test/data.test.js (REA-Y's line = the class trait's, its added line +12, REA-X 60; no module line anywhere
differs from its class trait only in its own added line's numbers — fails on the old data); test/content/
kits_alt_t5.test.js (the reaper heal 50 with REA-Y / none, 60 with REA-X); test/ui/loadout-rows.test.js (the
干员调配 module card: 特性 50, 特性追加 +12). GOLDEN_FULL: unchanged.
- Conflicts (the branch started at ae22335c): spec.js / protocol.js b.progress (resolved from fb7-ui-backlog + leaksBy),
gameLogic.js / game.js exports (device tips + normalizePersonalChoice), docs/PLAYING.md / design/network.md lines
merged against the base, META.md (画卷 note + 教鞭), test/docs-consistency.test.js pin (both fields).
- Grok review: MERGE AFTER FIXES — the HIGH (a forged boss result paid the cards) fixed on the branch (66a228c6), the
META wording (1ab12763).
- Golden: matches 10 (the 杜宾 bot picks and the guard placement); union: coop3-ABYSS-5 differs from HEAD only in ai_2's
dmgDealt (1238011 → 1186269), no outcome change — HEAD's side had moved it, the new placement applies on that path.
- Broad node --test 5623 (5621 pass, 0 fail, 2 skipped); lint 0 errors; typecheck; i18n strict clean.
Seen (review of this branch): the §2.5 bullet said an engine-played seat, "one that left" among them, picks like the bots do,
and design/match.md called the finalizer a "disconnected-seat" one. onLeave -> _quit -> PlayerState.eliminate clears the
bounties and personalChoice (META §1.3 already says the offer goes with the seat); a merely disconnected human is not bot
controlled — it waits for the prep's deadline like for any prep action. The sentences now say that. No code change.
Seen (review of this branch): the Final Assault / Hidden Core path trusts a client's b.result — validateClientResult skips
the leak multiset for boss fields and nothing re-simulates — while the team LP a field costs arrives through b.progress.leaks.
Commit 05eb2b80 paid the perfect-payout cards from the result's leaked list, so a modified authority could have the server
charge a leak and then report two empty lists (LP 58 -> 57 charged, both seats +2: FORGE_BOTH_EMPTY) or pin the leak on the
partner's list (the forger +2, the partner 0: FORGE_PAY).
Now: b.progress carries `leaksBy` (shared/protocol.js, optional; spec.js battleProgress for a boss / hidden field): per
player, the LP of the leak entries the result will show (compactResult's list, lpr clamped as on the wire); `leaks` minus its
sum is the leader's own "扣除目标生命" effects (boss "Doom" / 斥退 / 萨米 Doom), which are no leak and must not cost a perfect
seat its card. The browser runner and the test SimClient send it; the server keeps the highest value per player (f.leaksBy).
Match._bossLeaksAgree (called by _finishFinal) pays a seat of a client-reported field only when (a) the LP the authority
reported was split at all (leaksBy seen whenever lpReported > 0 — a report without the split explains nothing) and (b) the
seat's leaked list shows at least the leak LP reported for it. A server-run field (no client, a takeover) is the truth itself.
Not stopped, and said so in the docs: an authority that lies consistently (a split that blames the partner, a leak labelled as
the leader's effect) — the boss path has no second source; what is caught is a result that contradicts its own progress.
Tests (test first; the three forge tests failed on the parent with [2, 2] / [2, 0] paid, the runner test on the missing field):
clientCombat.test.js — charged leak + two empty lists -> 0 / 0; the leak moved to the partner -> 0 / 0 (with and without a
split); honest split -> the clean seat paid, the leaker not, a leader's effect LP costs nobody; two honest SimClients end to
end; the protocol field; runner.test.js — the browser runner's boss progress carries leaksBy; docs-consistency pins the
b.progress signature. Grok's two repro scripts now print +0 / +0.
Golden: unchanged (matches 18 / full 283 scenarios match; every golden boss field is server-run).
Seen: the sim moved a displaced enemy at once and it walked / swung again on the next tick; players compared with the
official game (PR #392: 「被推/拉完的敌人会先站住一小会儿再走」). Research (scratchpad report r3): PRTS 失衡位移机制 — a force > 0
puts an enemy in the UNBALANCE state machine with a 0.1 s 失衡硬直 floor, ending when its speed is ≤ 0.1 m/s, on 浮空 or a
forced state change; 异常效果图鉴/失衡免疫 「失衡期间无法自主移动、发动攻击、使用技能」; 异常效果 — not an abnormal status (水遁忍者's
check still runs); 游戏数据基础 推力-位移近似对应表 位移时间 (whole frames at 30 fps); 推与拉 拉力 「作用时间默认为 1 s;若本次受力
等级 < −1,作用时间改为 0.5 s」, 急停 keeps the state to the window's end; 特殊机制 静态刚体 (0.1 s 保底); 术语释义 浮空.
Now: constants.js PUSH_UNBALANCE {−2: 0.2, −1: 0.4, 0: 0.8, 1: 0.9, 2: 32/30, 3: 35/30}, PULL_UNBALANCE 1 /
PULL_UNBALANCE_WEAK 0.5, UNBALANCE_MIN 0.1. battle/displacement.js: push() holds for its level's row (the −2 correction
included), kept when the 特效 column or a wall shortens the slide [ASSUMED for the wall], the 0.1 s floor when a wall
stops it at the first step [ASSUMED]; pull() holds for its window whatever the travel (≤ −3: none; an enemy the puller
blocks: still no pull [ASSUMED, unchanged]); a 静态刚体 (data staticBody, not 失衡免疫 noDisplace) hit by force level
≥ −2 gets 0.1 s and no movement (_staticForce); _unbalance sets enemy.unbalanceUntil (a longer running one kept) and ends
the attack clip's stand. ai.js updateEnemy: no walking (恐惧 / 诱导 too — PR #392's fear exemption is not taken), no new
normal attack, a swing short of its frame cut, 浮空 clears it; a 重生 clears it (archetypes.js reborn). Not a stun: no
status, no immunity, SP and the attack cooldown run. No global skill lock: enemy abilities keep ticking ([ASSUMED] scope).
The move stays instant (the unbalanced() talents keyed off the jump unchanged). The displace fx carries `dur` (game s);
render/units.js slideTo divides it by ctx.animRate (2× default: a 0.8 s push slides 0.4 real s); DISPLACE_SLIDE·√D is
only the fallback for an fx without it. Not taken from #392: UNBALANCE_TRAVEL 0.6387·√tiles / the client's 0.3194 (derived
from the page's μ = 0.5 单位假设, one frame high on every push row, the wrong shape for pulls) and the fear exemption.
Tests: test/sim/unbalance.test.js (8: the push rows and walking after them; no attack in the hold, a feared enemy held;
the −2 correction's 0.2 s; 特效 / wall-shortened rows and the first-step floor; pull windows incl. the 急停 and ≤ −3; 静态刚体
0.1 s without movement, 失衡免疫 never; 浮空 clears; the fx `dur` — the file fails on the parent); test/render/unitview.test.js
(+1: the slide at 2× for a push and a pull, the fallback — fails on the parent); feedback1b-displacement B5 (the drone is
now held 0.1 s per force, never moved). test/sim + test/content + test/render: 3433 pass. Docs: SIM.md (push / pull,
displace, assumptions), PLAYING.md 推开 / 拉拽, research 13 §0 / §5.
Golden — every moved battle has pushes / pulls (probe of the callers) and its enemies are now held; outcome moves:
roster-002 (野鬃: killed 29→33, leaks 15→11), roster-003 (野鬃: deaths 3→7), roster-007 (薄绿: killed 110→118, leaks 53→47),
roster-008 (薄绿: 45→47 / 42→40), roster-010 (歌蕾蒂娅: killed 45→44, leaks 57→58), roster-011 (莫斯提马: 78→79 / 4→3),
roster-031 (钩索师 pulls, tier4: killed 64→72, leaks 12→4, deaths 6→5), roster-041 (钩索师: deaths 5→4), roster-043 (山:
deaths 15→14), roster-044 (山: killed 107→103, leaks 12→16, deaths 15→14); bond-deputShip-low (薄绿: 18→21 killed, leaks
3→0, deaths 1→0), bond-kazimierzShip-high (野鬃: deaths 3→2); boss-boss_3-pair (锏: p2 deaths 3→2), boss-boss_4-pair
(莫斯提马 / 歌蕾蒂娅: killed 11→12, boss HP 7442429→7375628, p2 leaks 2→1, deaths 19→18), boss-boss_6-pair (boss HP
6932482→6865243, p2 deaths 19→17), hidden-boss_8-pair (boss HP 12585770→12585798), hidden-boss_9-pair (锏: boss HP
3630612→3636991); standin-05 / 08 / 10 (预备干员-特种 pulls: killed 5→8 / 42→44 / 28→29, leaks down by 3 / 2 / 1);
diy-101 (傀影: killed 22→21, leaks 92→93), diy-114 / 115 / 116 (黑键: 106→99 & 83→90 / 154→156 & 78→70 / 3→2 & 120→121),
diy-137 (预备干员-特种: 68→79 / 79→68); matches through those battles — coop2-HARD-4, coop2-NORMAL-14-standins,
coop2-NORMAL-3, coop2-NORMAL-70-diy, coop3-ABYSS-5, coop4-ABYSS-7, solo-ABYSS-2 (LP 2→4 at round 10), solo-HARD-2
(ai_0 now survives round 13: eliminated 13 → alive), solo-HARD-9-boosted (LP 399→398), solo-NORMAL-1. Other moves (damage,
heals, snapshots only): roster-009/012/017/019/032/039/040/042/046, 13 bonds, boss-boss_2-pair, hidden-boss_10-pair,
standin-03, diy-053/084/085/102/103/108/109, coop2-ABYSS-10-boosted, solo-ABYSS-1.
PRTS 卫戍协议:盟约 PRTS盟约记录, 法术编纂集, 画卷 备注: 「选择策略<夕>时可获得<br>使用后销毁,获得的装备为未装备状态」.
Without a promotion the 画卷 handler still equipped every copied item that did not merge (an advanced item, a
变形同构体) onto the copy in the hand (the promotion case was fixed for #389). Now each copied item is only gained
(ctx.grantItem → acquireItem): the hand, overflow temp, and with both full it is destroyed with the usual toast
「整备区已满,获得的装备已销毁」, as for any gained item. A copied normal item that completes a pair still merges at once
(the golden in the hand), and the copy itself still follows the chess gain rules (a full hand and temp refuse the
scroll: HAND_FULL, the scroll kept). Full-hand check: a full hand with 3 free temp slots holds the copy and both
copied advanced items; with 2 free slots the copy and one item fit and the other is destroyed with the toast (before,
both rode on the copy).
Docs: META.md (Arts list), research/04-items.md (画卷 copy). The content test that pinned the old behaviour
(test/content/items.test.js: 变形同构体 "the copy wears it too") now expects it in the hand.
Tests: test/match/scroll-copy-merge.test.js +3 (no promotion, normal and elite targets with advanced items; the full
hand at 3 / 2 free temp slots) — all 3 fail on the parent; every test mentioning 画卷: 177 pass. Golden unchanged.
- A 干员调配 bullet: the three tabs; the list (a row per operator: 技能, 模组 精锐, 潜能 · 练度 — tap a choice in the row,
the portrait / name opens the detail; below 1000 px the detail covers the list); 潜能 1–6 and the four 练度 tiers per
operator (normal, elite and 自选 share them), the owner's default (潜能 6, 精英阶段2-60级), the official rule (the
player's own potential; the 自持有 bonus table, a multiplier of its own; an unowned 特许 operator at 潜能1 without
bonus — set it by hand), stand-ins and prototype picks have neither, cards follow the settings, a teammate's units
theirs; changeable until the briefing ends; 恢复默认 / 导出 / 导入 / the button's count include them.
- 干员持有 now starts from the second tab; 自选编队: an owned pick's 潜能 / 练度 on its slot card.
- The detail card's base values: at the operator's 潜能 / 练度 (no longer 「满潜能 for everyone」, 0.2.1 §26.14); summons
take the owner's potential for their talents and no 练度.
The owner's decisions of 2026-10-08: 「D12 [PR #301] 可以做,具体布局怎么优化你来决定,他那样确实方便,但是三个竖排有点乱影响
观感」 and 「D7D8调配干员里自己设置吧,默认满潜满加成」. The quick choices are PR #301's idea (by @farridge); the layout is new:
one list instead of three columns of cards.
- screens/loadout.js: RosterHead + RosterRow — every row the same four columns under a sticky head (干员 | 技能 |
模组 精锐 | 潜能 · 练度): portrait / name / bonds (selects, opens the detail), three skill slots (an empty one for a
two-skill chess; the label says slot, name, SP, duration — quickSkillTags), the elite's modules (不装备 first), the
潜能 / 练度 selects; a quick choice goes through setChoice / setEntries like the detail and keeps a phone on the list.
The detail's 普通 / 精锐 skill toggle is one preview for the skills, the 特质 and the rows. The detail opens with the
潜能与练度 section; 恢复默认 / 全部恢复默认 reset the settings too (全部: the roster's operators — a 自选 pick's stay
with the 自选编队 tab); 导入 reports a settings-only import; ←/→ stay inside a focused row's choices.
- screens/cultivation.js (new): CultivationSelects (two compact selects; quiet at the defaults, mint when changed) and
CultivationSection (潜能 1–6 and the four official tiers — effects.json names and effect texts — the rule in one line,
the stand-in note for a chess marked not owned).
- screens/diy.js: an owned 自选 pick's slot card carries its operator's selects; a prototype says it has neither.
- css: the head and the rows share one grid (`--cols`), 32 px quick targets (40 px on a coarse pointer); the detail
slides over the list below 1000 px (was 760 px); below 700 px the row's bonds give way to the name. Headless check at
1920 × 1080, 1366 × 768, 844 × 390 and 667 × 375: no sideways overflow, the head's columns equal every row's.
- The 局内数值 caption says 含潜能与练度 when the numbers carry both (effects.json loaded); the overlay's note says the
skills, modules, 潜能 and 练度 can be set (the operator's in-match level cannot).
- i18n: 21 strings in the four packs (the tier names, short tier labels, the notes, the reset confirm, the import toast).
- Docs: design/match.md (the list, the 潜能与练度 section, 导出's `ops`), ARCHITECTURE.md (screens/cultivation.js).
- Tests: test/ui/loadout-rows.test.js (new, 7): a row's pick / skill slots / modules / aria-pressed / callbacks and the
shared 精锐 preview, its selects (values, options, a change, the changed mark, a stand-in), the head, the detail's
section (tiers from effects.json, the effect text, the stand-in note), a 自选 owned pick vs a prototype, 局内数值 at
潜能1 / 未精英化 and at the defaults with its caption, the css grid and breakpoints. e2e (not run here; their steps
replayed with a headless page): the row pick selector, the phone test (one list, 40 px targets, the detail slides
over, back to the list), the 局内数值 expectations ×1.1, a new 潜能 / 练度 flow (row select → stored, 部署费用 17 / 攻击
454; the detail's 未精英化 → 413; the row follows; 恢复默认).
- Golden union: coop2-ABYSS-10-boosted and coop2-NORMAL-14-standins differ from HEAD though the commit alone left them —
HEAD's side had moved both, and on the new paths 隐匿 enemies meet active 突袭 members (14-standins: raidShip on;
round 10 kills 26 → 27, leaks 3 → 2, team LP 39 → 40; ABYSS-10: a few 阿戈尔 layers, team LP 793 → 795); no outcome
change.
- Broad node --test 5575 (5574 pass, 0 fail, 1 skipped); lint 0 errors.
GitHub PR #281 by @IceCodeNew (this commit is substantially the PR, ported onto 0.2.2-dev; the field-detail range
idea after @BBleae's fork commits c6437f1 / b451bad, credited here instead of the PR's Co-authored-by trailers). The
unofficial skill-range preview the PR first had is gone (the owner's reply of 2026-10-07: only official-like
behaviour); what remains is the range the unit fights with now.
screens/game/range.js inspectRange resolves the open card's unit: an own board piece in prep (its loadout or
stand-in record, its facing — this replaces the prep selection's range effect); in battle or on a scouted board an
ally op / token on the field, at the snapshot's tile, with the live stats' range when there is one; never an enemy,
a dead or undeployed unit, or a scouted bench unit (UnitInfo `area` hand / temp — the tag the server sends since the
#385 fix; renderInfo now keeps it). shared/protocol.js unitStatsEntry gains an optional `dir` (the unit's facing now:
the snapshot tuples carry none and UnitInfo.dir is the facing at send time). tokenVariantFor moves to
gameLogic/loadout.js. Drag, the wheel and the pen clear the overlay.
Added on top of the PR: a test from the server's own scout m.field (board operator ranged, hand / temp none), docs
(PLAYING.md, design/client.md, design/network.md), comments.
Tests: test/ui/inspect-range.test.js (6), test/ui/playtest6_summons.test.js, test/match/unitStats.test.js;
test/ui/inspect-range.e2e.test.js (SP_E2E + CHROME_PATH: run here, 1/1). Golden unchanged (display; m.unitStats only).
- Conflict: docs/PLAYING.md 技能自动释放 bullet — both insertions kept (the 铁钳号 rule from fb7-owner-sim, the #298
pacing sentence).
- Grok review: MERGE AFTER FIXES — the HIGH #298 finding (the cast replaced the due attack) fixed on the branch
(db29acf9: per-skill nextCastAt; golden of #298 alone shrank from 84 scenarios to 2).
- Golden union: 4 matches differ from HEAD though the branch alone left them (coop2-NORMAL-3, coop4-ABYSS-7,
coop2-NORMAL-8-serverrun, solo-HARD-9-boosted): HEAD's side had moved all four (#333's shop RNG …) and on those new
bot paths the merged rules apply — the diffs are in-battle layer gains / damage (卡西米尔 50 → 45, 精准 80 → 76,
叙拉古 176 → 168, kills ±2, solo-HARD-9 LP 397 → 399), no outcome change.
- Broad node --test 5571 (5570 pass, 0 fail, 1 skipped); lint 0 errors; typecheck.
GitHub #316 (warpgenius): "Raid operator doesn't deploy to invisible enemy units, it does in official". The owner's
decision of 2026-10-08 from their official play: a 突袭 member considers 隐匿 (unrevealed) ground enemies both as landing
targets and for 「若范围内没有敌人」. Until now raidTargets and the poll used canTargetEnemy / enemiesInKeys, which leave
隐匿 out (the bond text only says 敌人 / 地面敌人).
content/bonds/addon/battle.js: raidGroundEnemy accepts what canTargetEnemy (ground) accepts, plus a 隐匿 ground enemy that
only its 隐匿 keeps out; asleep, untargetable and flying enemies stay excluded (skipEnemy too). raidStealthInRange: a 隐匿
ground enemy on the member's range stops the jump, so a ready member next to one does not hop every poll. The landing
rules are unchanged (the target in range from the landing tile; a melee member's blocking tile first).
Golden: solo-HARD-1, solo-ABYSS-1, coop2-NORMAL-3, coop4-ABYSS-7, coop2-NORMAL-70-diy — their 突袭 members (斯卡蒂, 瑕光,
宴, 伊内丝) now jump to 隐匿 enemies (潜伏者, 隐形弩手组长, …) or stay because one stands in their range.
Tests: test/sim/feedback7-raid-stealth.test.js (4; 2 fail on the parent: the jump to the only landing spot beside a 隐匿
enemy, and staying with one in range while a visible enemy waits elsewhere).
- Grok review: MERGE AFTER FIXES — the end-portal wait (0 s → 3 s, the data's modal crossing) fixed on the branch
(1205336a). solo-NORMAL-1 still fails the Hidden Core (explained in the branch commits).
- Golden union OK; broad node --test 5517 (5516 pass, 0 fail, 1 skipped); lint 0 errors.
GitHub #394 / PR #395 by @Xinglan233. The wheel's capture listener committed the previewed direction on Enter whatever
had the focus, so Tab to 准备 / 设置 / the exit button / ✕ and Enter deployed (the PR let only a focused ✕ keep its
native Enter). Now gameLogic facingEnter decides: Enter on ✕ cancels; on any other control (button, link, form field,
ARIA control) it is swallowed — the wheel already holds Space and the shortcuts; elsewhere it commits the previewed
direction, and does nothing while none is previewed. The wheel (role="dialog") takes the focus when it opens
(tabIndex -1, no focus ring over the screen), so a button focused before the drop cannot swallow the confirming Enter
and the first Tab reaches ✕; nothing restores the old focus when it closes (a restored 准备 would take the next key).
Docs: PLAYING.md's key paragraph. Tests: test/ui/feedback5-hotkeys.test.js (facingEnter and its wiring, 2 new);
test/ui/facing.e2e.test.js (SP_E2E, the PR's drag / Tab flow, 4 cases: run here in headless Chrome, 4/4 pass, 4/4 fail
on the parent — Enter on the focused exit button deployed). Golden unchanged (client only).
- Grok review: MERGE AFTER FIXES — the HIGH finding (skills opened on the device but spending nothing) fixed on the branch
(69d791f9: skills.js allyTargetsOk, per-kit allyTargets flags).
- Golden union OK; broad node --test 5500 (5499 pass, 0 fail, 1 skipped); lint 0 errors.
GitHub #385 / PR #387 by @zasx77771. The prep scout sends the hand and the 临时整备区 as units, and ownerBoard put every
op unit on the popup's synthetic board, so the popup marked bench operators 在场 and its 成员 header disagreed with the
server's ring (炎: ring 1, header 3; 远见: ring 2, header 3 with the temp copy).
The scout's units now carry `area` 'board' | 'hand' | 'temp' (the server tags from PR #387; the tag survives the
boss-half remap, the row does not). ownerBoard sorts them into board / hand / temp instead of dropping the bench as
the PR did: dropping the hand would break the bonds that count the 整备区 (投资人 / 远见 / 奇迹 — a hand-only 远见
member: ring 1, header 0). bondMembers / memberHeadCount then count exactly as the player's own popup: hand = owned
and in hand, temp = owned only. A unit without an area (battles, older servers) stays on the board.
Docs: PLAYING.md's 观看谁 paragraph (the bench is listed in prep); comments in bondStrip.js / game.js.
Tests: test/ui/watch-bonds.test.js (36; 2 fail on the parent), test/match/prep-bench.test.js +
feedback5-boss-pair-prep.test.js (area tags; 4 fail on the parent). Golden unchanged (views only).
Review of fb7-gh-sim (MERGE AFTER FIXES, high): 4128965a wrote u.atkCd after such a cast and gated the tick casts on it,
so each cast took the attack that was due — 度算浪波 with 迅捷: 15 casts, 0 attacks in 20 s (silenced: 15 attacks);
冲锋号令·γ型 and 医疗无人机 lost one attack / heal per cast; after an idle cast the next attack waited the armed cooldown.
PRTS 度算浪波 is an automatic instant throw, not an attack.
Now (skills.js): such a cast (instant / charges, no spec.attack, not the DEFAULT cast right before an attack) sets
skill.nextCastAt = now + the unit's attack interval, and the casts checked in skill.tick wait for it; the attack cooldown
is not touched (reset on deployment, like opReadyAt). Measured (20 s, 引星棘刺 S1, 迅捷 229 layers): 12 casts — the first at
4 s as SP fills (#124), then one per 1.36 s — and 14 attacks, as many as silenced; without 迅捷 3 casts and 14 attacks; an
idle cast does not delay the next attack (1 tick). [ASSUMED] one attack interval stays.
Tests: bonds_addon «迅捷 refills 引星棘刺 S1 at once …» now asserts the casts take no attack (attacks = the silenced run's;
fails on 4128965a: 0 vs 4, and before it: 180 casts in 6 s); op_wang waits skill.nextCastAt; playtest6-skills (锏) back to
its original timing. test/content + test/sim: 2835 pass. Docs: SIM.md §7, PLAYING.md 技能自动释放 (no attack is taken).
Golden: against 4128965a's files roster (21) bonds (9) fields (9) matches (16) standins (6) diy (22) scenarios move back; against the
same code with the gap disabled only roster-033 and roster-034 differ (引星棘刺 S1 recast within one interval) — the
84-scenario move of 4128965a came from the attack cooldown, now gone.
Substantially PR #301 by @farridge (its non-layout part, ported; the owner's decision of 2026-10-08: 「可以做,具体布局
怎么优化你来决定」 — the card layout is redesigned in the next commit):
- ui/detailPanel.js exports GarrisonBlock; screens/loadout.js LoadoutGarrisons renders the 特质 of the form the detail's
普通 / 精锐 skill toggle shows (garrisons.json `descRaw` with its trigger markers, 「<进入休整期时>」 …) at the top of the
detail; garrisons.json loads with the overlay; its reading sizes in loadout.css.
- ui/loadoutModel.js quickSkillTags: SP recovery (攻回 / 受回 / 自回 / 被动, 部署触发 for a deploy-and-expire passive) and
duration (s, 弹药, 常驻, ∞, 瞬发) from the generated SkillRecord, never from a localized description.
- i18n: the six new strings in the four packs (en from the PR; ja / ko / zh-TW added).
- Tests: test/ui/loadout-quick.test.js (from the PR, 4): quick tags of 空弦 / deployment skills / ammo / infinite, the 特质
blocks of 空弦 / 星熊 / 缄默德克萨斯 both forms; loadout-stats.test.js reads the new import.
The Grok review of fb7-fields (MEDIUM): portalPickup spliced DISAPPEAR and APPEAR with nothing between, so the enemy
vanished on [1,3] / [1,17] and stood on [5,10] in the same tick, targetable at once, while the data's explicit
crossings of the same tiles hide it: WAIT 3 s in 85 of the 95, 5 s in 9, 1 s in 1. portalPickup now splices a 3 s
WAIT (PORTAL_WAIT, the modal explicit wait) between them; [ASSUMED] the 3 s, no source times this crossing. Docs: the
SIM.md sentence and the portalPickup comment.
Tests: test/sim/portal-appear.test.js — the two route-end tests record the test enemy's hidden spans: one crossing,
entrance → [5,10], hidden 3 s (± a tick); both fail on the parent (0.000 s). 9/9.
Golden (against the 0 s version; the rounds of every match unchanged):
- matches solo-NORMAL-1 (Hidden Core boss_8 胄, act1autochess_h08_01_s): still failed — hiddenCleared false, team LP 0,
14 rounds — with the leader left at 63897 instead of 99894 (cleared with 26 LP in 0.2.1). The same course as with
0 s: the third vtsk, which leaked at the portal at 105 s in 0.2.1, comes out of [5,10] 3 s later, walks through the
team's line and leaks at the door at 185.5 s (170.2 s before); 0–2 of the 8 operators stand between 100 and 320 s, the
leader stalls near 108000 from 280 s, the overtime drain from 300 game s takes the 25 LP left by 350 s while the
redeployed operators bring the leader to 63897 (notes trace-hidden*.mjs).
- matches solo-HARD-1 (Hidden Core failed as before): pool left 2839368 → 2982555; solo-HARD-9-boosted (cleared): team LP
361 → 376 (383 in 0.2.1); coop2-NORMAL-14-standins (Final Assault lost as before): pool left 97588 → 108473.
- fields boss-boss_2-solo (leader left 2763024 → 2757148), boss-boss_4-solo (3759921 → 3757752), boss-boss_6-pair
(killed 6 → 5, one hllevi_2 still walking at the 200 s cap, both players still perfect; leader 6972383 → 6932482),
boss-boss_6-solo (3904791 → 3907393), hidden-boss_8-pair (the vtsk that comes back near the cap is still inside the
portal at 200 s: one appear fewer, the last snapshot hash), hidden-boss_8-solo (killed 8 → 6, leaks 10 → 12),
hidden-boss_10-solo (killed 11 → 10, leader left 7323370 → 7333760).
- unchanged by the wait: solo-FUNNY-2, coop2-ABYSS-10-boosted, the other fields, and the roster / bonds / standins / diy
families.
Seen: 蛇屠箱 and the seven other operators with the range 0-1 (角峰, 古米, 泡泡, 折桠, 菲莱, 塞雷娅, 余 — 16 chess records, all
melee blockers with block 3, the elite 角峰 4) block and hit only on the tile they stand on. The layout planner maximizes an exposure model whose
kill estimate saturates in a full layout; the tile then falls to a 1e-6 rng tie-break, so a guard stood off the road, where
it neither blocks nor hits (reproduced: planLayout with six of them on act2autochess_m04, seed 11 — 蛇屠箱 off the road; over
40 seeds of the eight stages 16 of 320 guards stood off the road: 11 because the eight guards outnumber the 7 free road tiles of
act1autochess_m03 / act2autochess_m02, 5 avoidably — after the change only the 11 are left).
Official: excel range_table "0-1" is the single grid (0,0).
Now: planLayoutSteps keeps the best road tile (model.ground) for a blocker whose deployed range grid (rangeRec: its
loadout's) is exactly one (0,0) grid, and takes it before the overall best; with no free road tile the best legal tile as
before (a high-ground 「可以放置于远程位」 blocker keeps its own preference first).
Not taken from the PR: the scarce-card penalty of buyScore, the item scoring (人事部文档 "only with nine 炎" contradicts the
official 部署上限变为 9), the 神秘 morph fit and the draft yield — AI heuristics with no official text, and the golden
swing they caused (a solo match that cleared the Hidden Core no longer did) came from them, not from the placement.
Golden (matches only; every other family unchanged, --twice identical): solo-FUNNY-1 (from R7: boss pool left 74740 -> 71550,
still a loss in R8), solo-NORMAL-1 (from R4: same win, Hidden Core cleared, team LP 26), solo-HARD-1 (from R4: boss pool left
3046117 -> 2940542), solo-ABYSS-1 (from R4: 2303334 -> 2281356, still a loss in R13), coop2-NORMAL-3 (from R4), coop2-HARD-4
(from R4), coop4-FUNNY-6 (from R3: virtualMs 128500 -> 127000), coop2-NORMAL-8-serverrun (from R3), coop2-NORMAL-70-diy (from
R6): rows, stats, lineups and rng draws differ, wins, losses and rounds passed do not.
botbench A/B on 12 seeds each (solo HARD / ABYSS, coop4 NORMAL / HARD): wins 1 / 0 / 11 / 3 both before and after, rounds
passed 13.00 / 11.92 / 14.00 / 13.00 -> 12.92 / 11.92 / 14.00 / 13.17, LP left 18.7 / 6.5 / 80.1 / 27.9 -> 18.8 / 7.4 / 83.7 / 28.0.
Tests: bot.test.js — the PR's saturated-layout case (蛇屠箱 blocks and hits only on a road tile, a real battle on both tiles;
the road full: a legal fallback) and every stage over 40 seeds (the eight guards fill min(8, free road tiles) road tiles);
both fail on the parent.
- Conflicts: the stats entry (merged fb7-stats) and the settings button both sit in the lobby / room top bars — both kept;
public/js/ui/settings.js (#321 diagnostics, voiceLang, SettingsButton); docs/PLAYING.md table rows resolved line by
line against the merge base (#368, #354, #333 edits kept); docs/SIM.md / design/network.md snapshot field lists
(resolved + ammo / wolves / neg / stand / standCut); design/client.md Screens line (#368's AI switch kept).
- Grok review: MERGE AFTER FIXES — the shared-field capsule finding fixed on the branch (1a880699).
- Golden: roster 23, bonds 2, fields 11, matches 18, diy 11 — #308 totals / snapshots and #333's shop RNG (union OK);
broad node --test 5473 (5472 pass, 0 fail, 1 skipped); lint 0 errors; typecheck; i18n strict clean.
Grok's review of the 铁钳号 change (HIGH): the device opened every non-heal skill, but kits that pick their victims
with enemiesInKeys only were spent on nothing — 蕾缪安 S3 went active with 0 locks, 艾丽妮 S3 and 陈 S2 struck nothing,
引星棘刺 S2 threw at the empty far tile. Audit: every skill of every chess (loadouts) and of the 自选 pool, placed with
only the device in range; of the 351 skills the device opened, 67 never touched it.
- skills.js `allyTargetsOk`: the device counts for a skill's start only if the skill acts on it — SkillSpec
`allyTargets` (true / false) from the kit, else [ASSUMED] a skill acting through the unit's own attacks (timed and
still attacking, or a cast that is the next attack) on a unit that attacks. An onStart-only cast or a no-attack timed
skill picks enemies itself and waits for one (德克萨斯 S2 剑雨, 锡人 S1/S2, 铃兰 S3 … about 50 skills).
- Spend on the device (allyTargets: true; its kit cancels the hits, its 受击回复 counts): 蕾缪安 S3 locks it and spends
her bullets (ammoUsed, so the per-bullet layers come) and shells it; 艾丽妮 S3 strikes it; 陈 S2 draws on it after
every enemy; 引星棘刺 S2 throws onto it and its pulses hit it. queries.js allyTargetsInRadius for their areas.
- Explicitly closed ([ASSUMED], allyTargets: false): 普罗旺斯 S2 (never shoots above 80 % HP), 卡涅利安 S1 (charged: no
attack), 山 S2 (blocked enemies), 余 S2 / S3, 迷迭香 S3 (blocked only), 荒芜拉普兰德 S1 / S2 (drone locks), 淬羽赫默 S1–S3
and 遥 S1–S3 (heal attacks), 机械师 S3, 珊比 S2 / S3, 陈 S3 / 艾丽妮 S2 (their guards already refused).
- 雪猎's empty-magazine cast back to enemies only (its S2 does not act on the device).
Re-audit: 281 skills open on the device alone and all act on it (蕾缪安 S3 through her locks); none of those that
acted on it before stopped. Docs: SIM.md, design/engine.md, PLAYING.md.
Tests: test/content/feedback7-allytarget-kits.test.js (5; all fail on the parent).
The owner's decision of 2026-10-08: 「调配干员里自己设置吧,默认满潜满加成」. Official: the account's potential (调度手册);
the 自持有 bonus to owned operators of the same name, initial and elite alike (activity_table cultivateEffectList).
- shared/protocol.js: room.loadout { entries, ops? } — `ops` = { [charId]: { potential?: 1–6, cultivate?: 0–3 } }
(isLoadoutOps, ≤ 256), checkLoadoutOps (strict like checkLoadout: an operator of the 干员调配 roster or the 自选 owned
pool — cultivationCharIds; defaults dropped, entries stored complete); absent = none set.
- server/lobby.js / net.js: checked with the entries (either refused ⇒ nothing stored), session.ops / seat.ops /
seats[].ops, match.setLoadout(playerId, loadout, ops) in INFO_CHECK only.
- PlayerState: ops re-checked and frozen (bots none); cultivationFor (shared/potential.js cultivationOf: null for a
补位 stand-in or a prototype 自选 pick); loadoutFor carries potential / cultivate; battleInput states `potential` /
`cultivate` for every owned operator (the chess as itself, an owned 自选 pick) — an old seat without ops and every bot
at 6 / 3; the 自选 summons' hand count follows the owner's potential (望's 棋子 6 below 潜能3); m.private.ops; the
scouting UnitInfo carries potential (below 6) / cultivate.
- tools/golden.mjs: the direct-spec scenarios field the default player (owned operators at 潜能 6 / 练度 3; stand-ins
and prototype picks neither), as PlayerState does.
- Golden: every battle with an owned operator moves — ATK / DEF / max HP ×1.1 from the first second (roster-001:
角峰 ends at 3527 HP instead of 3115; the first differing snapshot window is 0–10 s in every moved battle); the
matches part at round 1–8 (battle results, then the bots' boards). Unchanged: the 10 stand-in battles (a stand-in has
no 练度) and diy-135…138 (the four prototype 自选 battles). No engine error.
- Docs: design/match.md §16 (潜能 / 练度), design/network.md (room.loadout ops).
- Tests: test/match/potential.test.js (new, 4): the ops wire / data check, battle input of a human (settings, defaults,
stand-in none, elite shares the charId), an old seat and a bot at the defaults, spec → battle, scouting units; 自选
owned pick vs prototype and 望's hand count; Match.setLoadout in INFO_CHECK only, a refusal changes nothing.
test/lobby-loadout.test.js (+2): strict validation, storage beside the loadout, an older client's message, the real
match's briefing (m.private.ops) and lock. test/match/runner.test.js / diy-shop.test.js expect the new fields.
Seen: the Art 教鞭 (杜宾's strategy) drew three 战术特训 cards and added a random one — a documented simplification (docs/META.md
§2.5: a personal choice would need its own phase, protocol message, timer and AI takeover). Official: act2autochess
choice_event hunter_band_1 is a PERSONAL_CHOOSE; PRTS 下半 记录 §法术 教鞭 "使用后销毁,于3个战术特训的悬赏任务中选择一项". The
owner decided on 2026-10-08 to follow it. Taken from PR #353 (code and tests by @Sukvii), ported to the split 0.2.2 modules.
Now: ctx.offerBountyChoice -> Match.offerBountyChoice keeps up to three different mode-eligible cards (15 of 20 in 标准 /
险境, 20 in 绝境 / 终极; one meta-rng shuffle) in PlayerState.personalChoice, m.private only (same cards after a
reconnect); g.choice { idx, choiceId } confirms it (Match.pickPersonalChoice: owner, id, round, index; a request without
a choiceId is still the public draft, a stale id never falls through to it). No new phase or timer: Ready is refused while
it is open, the prep's deadline picks at random before the temp pieces and Ready, a bot or a 托管 seat picks with the bounty
scorer (botPickCard, the bots' seeded rng; at the prep's start, after the cast, before Ready; the scheduler's fallback
picks at random if a step fails). The client reuses ChoiceOverlay (personal: no order, no turns, the prep's countdown) over
a locked prep, the Ready reasons, the mock harness; six UI strings in en / ja / ko / zh-TW. An eliminated player's offer
goes with it; invariants.js flags an offer outside its owner's own open prep.
Not taken from the PR: its §25.24 in the released docs/history/0.2.0.md (the 0.2.2 section is the integrator's), its edits
of the Ready toggle beyond the personal-choice case, its tail placement of the i18n keys.
Golden: coop2-NORMAL-8-serverrun (ai_0 杜宾 casts 教鞭 from R2 on and now scores the three cards: teamLp 39 -> 34, virtualMs
121400 -> 118400, rng shop 511 -> 483 / bots 18063 -> 18070 / meta 112 -> 103) and coop2-ABYSS-10-boosted (ai_1 杜宾: virtualMs
121650 -> 124650, shop 590 -> 589 / bots 22075 -> 22096 / meta 141 -> 154); both first differ in round 2, every other scenario
and family is unchanged; node tools/golden.mjs --twice is identical.
Tests: items.test.js (three distinct cards, the tapped one applied once, nothing before the confirm, a refused cast keeps
the Art / count / rng / uid, an eliminated player's offer), economy.test.js (Ready refused, the deadline picks before
endPrep, solo prep untimed), connection.test.js (two players' offers, reconnect, foreign / duplicate / old ids, an offline
player's pick at the deadline), bot.test.js (托管 takeover incl. the failing-step fallback), bot-smarter.test.js (the scorer's
pick, deterministic per seed, no rng draw when nothing is pending), followups.test.js (R14 / R15), draft.test.js,
unitStats.test.js (protocol), hardening.test.js (invariant), gameLogic / playtest3 / playtest4 (view model, Ready reason,
ChoiceView, actions), mock.e2e.test.js and playtest2.real.e2e.test.js (browser, SP_E2E=1).
A PlayerBattleInput operator entry may carry `potential` (潜能 1–6) and `cultivate` (练度 0–3, the 自持有 bonus) —
the player's 干员调配 settings (the owner's decision of 2026-10-08). Specs without them field what 0.2.1 fielded, so
no golden scenario moves here; the match states both next.
- shared/loadoutRecord.js: a resolved loadout carries `potential`; loadoutRecord composes the record at it first
(shared/potential.js atPotential), then the skill / module as before.
- server/sim/simdata.js: getChess(id, { …, potential }), getDiy(slot, pick, potential) (an owned 自选 pick's form at it;
a prototype has none — `diyProto` on its record, shared/diy.js), getToken / getDiyToken compose the owner variant at
the owner's potential (talents; a 自选 summon's deploy limit / holding / count); the per-battle loadout view keeps it
per unit; DataSource.cultivateMul reads the effects.json CHAR_MAP record's char_attribute_mul.
- battle/players.js: `potential` composes the def (never a stand-in's); `cultivate` sets unit.cultMul — ×ATK / ×DEF /
×max HP in the unit's Πmul (units.js), a multiplier of its own, never summed with the 直接乘算 percentages (PRTS
盟约记录 「…与自持有干员的属性加成独立」); a stand-in, a prototype 自选 pick and a summon take none [ASSUMED for summons].
- spec.js keeps both only when well-formed (never on a stand-in or a summon); content/index.js passes the potential to
the loadout sync; the kits that read a summon variant raw (content/tokens.js ownVariant, summoner.js tokenStat,
多萝西, 麦克尼斯特) compose it at the owner's potential; UnitInfo carries `potential` (below 6) / `cultivate`;
unitStatsEntry counts the 练度 in the unit's own numbers (no green delta for it on the card).
- Docs: SIM.md §2 (Πmul), §9 (UnitInfo), §12.
- Tests: test/sim/potential-battle.test.js (new, 6): a unit at each potential (刺玫 413 / 17 / 70 s and 8 % at 潜能1;
the elite on 不装备), the four 练度 tiers and their product with a +20 % ATK bonus, HP at deployment and the card's
numbers, stand-in / prototype take neither and an owned 自选 pick both, Mon3tr / 望's 棋子 / “小自在” at the owner's
potential, the spec round trip (kept, dropped, the JSON spec gives the same result digest), two players with the same
chess at different settings on one field; test/sim/loadout.test.js expects the resolved loadout's new fields.
Counting rule (ui/stats.js countsTowardStats, the owner's decision of 2026-10-08: do not count the matches entered and left
right away, but keep them in the history): a settled match (m.result arrived) always counts; a match given up (放弃模拟)
counts once at least one of the player's battles had finished — a round cleared (the server's own rule, platform.js
_quit: leaving while round k runs has passed k - 1, in its settlement k) or already eliminated. Anything earlier is
history only: marked 中途退出 and 不计入统计, dimmed, listed in 对局记录, left out of every aggregate (games, wins, win
rate, strategy table, titles, rounds, time, combat totals); 总览 says how many were left out.
- A leaver gets no m.result, so the page records the match itself: quitMatch (放弃模拟) → recordQuit builds an
`end: 'quit'` record from the app state and a tracker (observeMatch, installed in main.js) that remembers when the page
first saw the match and the last round the player was seen alive in. No record for a spectator seat, a match with its
settlement already there, a match the page never saw; matches the server ended and closed tabs are not recorded.
Defects of PR #323 as submitted, reproduced (scratchpad/w7-stats-notes/repro-pr-bugs.mjs, loads-check*.mjs) and fixed:
- the title screen loaded all 16 game data files (5.3 MB) at boot: <StatsHost/> called useGameData() at mount; now the
host renders nothing until opened, the overview reads config / bands / assets and the settlement re-view loads the rest
when opened (the title screen loads the same 3 files as before);
- importing a file put the list oldest first (the page shows the first 20 as the newest and the cap would drop the
newest); the list is newest first everywhere, also when loaded from storage;
- recordResult wrote over the envelope of a NEWER build (a downgrade, against the PR's own rule); it refuses now, as
saveStats does;
- 1000 four-player records are ~7.2 M characters (the PR's ~1.5 kB/record is a solo match) against a ~2.5 M-character
quota: MAX_RECORDS 1000 stays, plus a 800 000-character budget (the oldest go first), and only the newest 30 records
keep their rows (lineup, bonds, teammates' names; the replay needs them) — older ones are compacted to the player's own
summary row (~620 characters), a refused write retries with half the history; unknown fields and oversized values are
dropped (a strict schema), import files are bounded (8 MB);
- the re-view clobbered the app store (match.public / match.result, the route, the music); ResultScreen is split into
ResultScreen (the store) and ResultView (props: result, myId, back label), so the stats page renders a stored match
itself; a 自选 pick and a 补位 stand-in were dropped from the stored lineup (the replayed card drew the plain
operator); DIFFICULTY_NAMES were not translated; a band cell was a display:flex <td>; the overlay stayed open when a
co-op match started; the 清空 two-step blink was replaced by the shared confirm dialog.
- Corrupt storage: unreadable data is moved to sp.pref.stats.damaged and the page says so; damaged rows are dropped and
counted; duplicates, an oversized or unsorted list are repaired on load; a pre-`end` (PR-era) envelope reads as settled.
UI: a full-screen overlay in the grammar of 干员调配 (top bar with 返回 / four tabs 总览 · 策略 · 战斗累计 · 对局记录 /
导出 导入 清空, a note line saying the data stays in this browser and that teammates' names are stored too), text sizes
with max(.xxrem, N px) for phones in landscape, a 50-row history page with 显示更多, a pass-rate bar, a per-difficulty
table in order; the entries use a new `chart` icon (not the ping bars), the title screen's sits beside the language menu
(the connection row has no room: it squeezed the gear and wrapped the status text), the lobby / room buttons show the
icon alone below 700 px (the room's title ran under the label at 640 px). docs/PLAYING.md §12, README row; 41 + 25 UI
strings in en / ja / ko / zh-TW (the PR's four unused ones removed).
Tests: test/ui/stats.test.js 44 / 44 (counting rule per phase, quit records, bounds, corrupt / old / newer storage,
import order, aggregation, the replay payload); test/ui/stats.e2e.test.js (opt-in SP_E2E=1, real server, headless Chrome:
seeded history on 1920×1080 and 844×390, the re-view, export / import / clear, corrupt and newer storage, and the real
flows: a solo match given up in the briefing is history only, one given up after round 1 counts, a settled match is
recorded once and not again after a reload) 6 / 6; lint 0 errors, i18n extract 0 / check --all --strict clean,
check:imports, typecheck, docs and version tests, golden unchanged.
- Conflict: public/js/ui/settings.js imports (store from #321 + VOICE_LANGS) — both kept. test/ui/playtest6-elite.test.js's
source pin of the onDetail line allows the new tap counter (bcee737f).
- Grok review: MERGE AFTER FIXES — the three findings fixed on the branch (bcee737f). Golden unchanged; broad node --test
5334 (the one failure was that source pin, fixed), i18n strict clean, lint 0 errors, typecheck.
The owner's decision (relayed 2026-10-08) overrides PRTS 下半/PRTS盟约记录 「部分盟约不会显示叠加层数,但是叠加层数的特质/策略/
装备等效果仍然对其生效」 for this strategy. bands/meta.js round_start_bond_check_gain_layer skips every bonds.json noStack bond;
the 「仅激活了1个盟约」 count stays the sentence's (every active bond), so: 独行 / 绝技 alone → nothing; 独行 + one layered
bond → the layered one +12 (count2), 独行 none. Repro: 独行 alone got 36, 绝技 alone 36, 独行 + 炎 got 12 each.
Only 余: 协防 (bonds/addon/meta.js deputShip, 「使所有已激活的盟约层数+N」) and 华法琳 【重点监护】
(prep_finish_char_bond_add_layer, a random operator's bonds, 无需激活) each pick their own bonds and call addLayers; they
share no helper with 余 beyond the generic Player.addLayers sink, which every layer gain uses — left unchanged, so both
still add to noStack bonds as PRTS says. Docs: PLAYING.md 层数. Golden: unchanged (no corpus 余 at round 8 with such a bond).
The owner's decision (relayed 2026-10-08): only the 速射手 air priority outranks the attacker's own blocked enemy — PRTS
索敌的概念 「阻挡(近战限定)→特殊优先级→…」 — while the user's playtest #6 rule 「阻挡了就一定要能打到」 keeps 'blocked first'
for every other attacker and priority. PR #220 by TsangAsuna (for #205) moved every special priority ahead of the block;
re-implemented for priority 'fly' only (targeting.js sortEnemyTargets: the fly key, then blocked, then the rest as
before) — the trait's priority and a skill's targeting priority 'fly' (新约能天使) alike. Seen (repro): 能天使 on a melee
tile blocking a walker kept shooting it with a flyer in range; now the flyer, and 远牙 (神射手, lowest DEF first) still hits
the enemy it blocks over a softer or a flying one. Docs: SIM.md (blocking), design/engine.md, PLAYING.md 阻挡.
Golden: roster-026 (流星, 速射手) and roster-037 (新约能天使's priority 'fly') now shoot flyers in range while blocking.
Potential becomes a per-player setting (the owner's decision of 2026-10-08: 「调配干员里自己设置吧,默认满潜满加成」;
official: 调度手册 「卫戍协议中干员潜能由自身已持有干员潜能决定」). 0.2.1 baked full potential into the data (§26.14).
- tools/build-data.mjs runs the chess / backups / tokens builders once per potential rank (ctx.potRank, 0–5) and keeps
the full-potential build annotated with what each lower rank changes: `potDown` { rank: { dotted.path: value } } on
chess records, backups.json forms and token variants (stats, statsBase; 望's 棋子 deployLimit / deckStack, 多萝西's
共振装置 count), and chained talent entries (`potMin` / `potBelow`: the candidates by requiredPotentialRank, the
fields a lower rank changes) in talents / talentsBase / modules[].talentChanges / variant talents. A self-check proves
shared/potential.js atRank rebuilds every rank exactly; the unit-form parity runs at every rank.
- shared/potential.js (new): atPotential / atRank, talentAtRank, stripPotential, and the 练度 helpers used next
(cultivateMul from the effects.json CHAR_MAP records aceffect_char_1…4, cultivationOf, cultivatedStats).
- composeTalents / composeUnitRecord return potential-resolved lists (no chains): a composed record is built at one
potential (its form resolved first).
- JSON compare: the new chess / backups / tokens minus the annotations equal the old files byte for byte; every other
data file identical; data/i18n/* rebuilt (the chained talent texts translated at the old rates: en 95.4 %, ja 95.2 %,
ko 95.2 %, zh-TW 90 %). Data 5.21 → 5.51 MB.
- Docs: DATA.md §2.3 (new), §2, §7, §14, §16, §18; research/03-operators.md; kits/README.md.
- Tests: test/potential.test.js (new, 7): every chess and 自选 form at every potential = the raw keyframes plus the
potential steps up to it, every talent = the last candidate with requiredPotentialRank ≤ the rank, 刺玫 at each
potential, summons at the owner's potential (Mon3tr 3 s / 1200, 望's 棋子 6 / 7, “小自在” 15), the 练度 table =
cultivateEffectList; data.test.js compares the composed talents without annotations.
1. VoiceGate.start no longer moves lastAt for VOICE_TAP_SLOTS: a tap still skips the 1.2 s gap but starts none, so a
部署 / 技能 / 接敌 the battle asks for once after a short 选中 line has ended plays where the last battle line's gap
ends (a 部署 at 0, a tap at 500: a 作战中 at 1300 was dropped, now plays; inside the 部署 gap it still waits).
2. Shop / reward cards hand only the chess id to the detail, so two cards of one operator (the pool deals duplicates)
had one 选中 key and the second tap said nothing nor replaced the first line: game.js numbers every card it opens
(`tap`, cardTap), resolveDetail keeps it, and detailPanel's new selectVoiceKey() includes it (a re-render of the
same tap keeps its key; pieces / units unchanged). Checked in the mock match: the second card of 歌蕾蒂娅 now calls
voice() (it made no call on the parent).
3. tools/package.mjs FULL_ZIP_JP_VOICE comment: 89,388,041 bytes = 85.2 MiB on disk, about 76 MB zipped (as ASSETS /
DEPLOY), not "~93 MB, barely compresses".
Tests: test/ui/audio.test.js (the tap-gap case; one earlier assertion that encoded the old gap after a tap replaced),
test/feedback7-voices.test.js (new: the card tap in resolveDetail / selectVoiceKey / game.js) — both fail on the parent.
§21.30's 0.2.2 note updated.