1131 Commits
Author SHA1 Message Date
sganggs c2822e6726 feedback7: §27.63 — the toast when 拟态物质 / 简易通讯机 / 寻呼模块 grant nothing (#401); the intro maps §27.61–§27.63 2026-10-09 01:43:00 +08:00
sganggs 775d4f8515 feedback7: merge fb7-dragsnap — a dragged operator stands on the tile under the pointer while that is a legal target, as in the owner's recording of the official deployment (the report in PR #403 by @bubububaoshe; the pick, highlight, wheel and drop stay the pointer's tile); §27.62; golden: unchanged 2026-10-09 01:43:00 +08:00
sganggs e1b75e1308 feedback7: merge fb7-tick — whole-frame attack intervals and time-SP costs take exactly that many frames, 1 s = 30 not 31 (PR #402 by @Cloudnyco, ported onto 0.2.2: 1e-9 on the ally / enemy attack countdown, the 双眼皮 turret and 寒檀's icicles, gainSp / _normalize; PRTS 作战机制 帧对齐); golden: every family moves (281 of 283, end-result flips in §27.61); union OK 2026-10-09 01:42:16 +08:00
sganggs 9405aae60c feedback7 R23-DRAGSNAP: a dragged operator stands on the tile under the pointer while that is a legal target (a board tile or a bench slot: render/drag.js dragStandTile), as in the official deploy drag (the owner's recording of 2026-10-09), and is held under the pointer elsewhere (an illegal tile, the shop bar); the pick, highlight, tileHover, wheel and drop stay the pointer's tile — the report in PR #403 by @bubububaoshe (not merged); §27.62; golden: unchanged 2026-10-09 01:38:57 +08:00
sganggs 6de5dec30c feedback7 R23-TICK: docs for whole-frame attack intervals and SP costs (PR #402 by @Cloudnyco) — SIM.md §2 / §7.1, design/geometry-time.md §4, DESIGN §27.61 and the intro's area map; golden: none in this commit (the previous one moved every family: roster, bonds, fields, matches, standins, diy)
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.
2026-10-09 01:37:32 +08:00
sganggs 227ab9e7ca feedback7 R23-TICK: an attack interval or a time-SP cost of a whole number of frames takes exactly that many — 1 s is 30 frames, not 31 (PR #402 by @Cloudnyco, ported onto 0.2.2); golden: every family moves (roster, bonds, fields, matches, standins, diy — regenerated in the next commit)
Seen (PR #402, measured again on 0.2.2): TICK = 1/30 is not a binary fraction. An attack cooldown counted down from 1 s
stops at 2.1e-16 after thirty steps and the fire check `atkCd > 0` waited a 31st frame; 300 time gains of 1/30 SP add up
to 9.999999999999975 and `sp >= cost` charged a 10-SP skill a frame late. Over the 265 golden battles 30.85 % of the
consecutive attacks on a whole-frame interval came a frame late (control, other intervals: 0.09 %), and 4 157
end-of-tick states sat within 1e-9 under a cost. The engine already took timers within 1e-9 as done (skill timeLeft,
buffs, every() / after(), enemy abilities, nextCastAt, the 失衡 end); these two did not.

Official: PRTS 作战机制/sandbox 「帧对齐机制对攻速的影响」 — one frame = 1/30 s; since 2019-12-24 a non-integer frame count
is rounded (四舍五入); an exact 1 s is 30 frames under any rule.

Now: ai.js attackCountdown(cd, dt) (left within 1e-9 of 0 is 0) for the operators' cooldown (the canAct guard kept) and
the enemies' (not while stunned or hidden; the 0.2.2 失衡 swing cut untouched) — an enemy's whole-frame wind-up is exact
too (0.4 s: 12 frames, 13 before). The same countdown for the two strict `cd -= dt; if (cd > 0)` attack clocks of the
same shape: the 双眼皮 turrets (content/devices.js; BAT 4 s: 121 → 120 frames) and 寒檀's S2 icicles (0.5 s: 16 → 15).
SkillRuntime.gainSp takes SP within 1e-9 of the cost as full for every charge (a tiny negative remainder → 0);
_normalize compares the same way. Left as they are: a non-integer count still ends on the frame that crosses 0
(⌈30 × interval⌉ — the official 四舍五入 is an older, separate difference); the count-up content clocks that restart from
0 (bosses.js self-timers, kit auras — PR #402's list); attack / hurt SP (+1 on an integer cost) unchanged.

Tests: test/sim/tick-tolerance.test.js (new, 7: the PR's four adapted — residue, attackCountdown over bat 0.1–10 s ×
14 ASPD = ⌈30 × interval⌉, 1 s operator every 30 / 3 s ranged enemy every 90, time SP 4/10/16/20/30/50 at exactly 30 × N
gains for one and two charges — plus the 1.2 s / 1.25 s control (36 / 38), the turret at 0 layers (120) and 寒檀's
icicles (15); the four engine tests fail on the parent: 31 / 91, cost 4 at 121, 121, 16). The PR's seven test edits,
re-run: the same 8 assertions failed on the new timing and each edit passes on the parent and on the fix —
feedback2-surtr-stealth (count once no arrow is in flight), kits_alt_t2 拉普兰德 S1 (dummy ATK 300 → 250: 120 × 15 = her
1791 HP by 59.5 s), kits_alt_t6 (medic smoke at half HP; 余 S1 up to the first 燃烧 burst), op_jesca2 (the T6 shield
returns a tick after the cast), op_lmlee (hits before the burst), op_logos (count after the clear), op_narant (the first
attack thrown after the new enemies). node --test (with the golden of the next commit): 5703 tests, 5701 pass, 0 fail,
2 skipped; eslint 0 errors (135 warnings, the parent's 135); typecheck, check:imports, smoke ok.

feedback7 R23-TICK: golden for whole-frame attack intervals and SP costs (PR #402) — 281 of 283 scenarios move: roster 49/49, bonds 46/46, fields 22/22, matches 18/18, standins 10/10, diy 136/138; solo-NORMAL-1 now loses its Final Assault (below)

npm run golden:update on the previous commit. Every family moves because both sides attack on time (more enemy kinds
than operator forms sit on whole-frame intervals: 3 s, 4 s, 1 s …) and time SP charges a frame sooner. Measured over
the 265 battles: whole-frame consecutive attacks a frame late 30.85 % → 0.04 % (control 0.09 % → 0.13 %; 1 s gaps at
30 frames 256 → 3 776 of ~3 800, 3 s at 90 frames 0 → 6 657 of 6 657), SP float-short states 4 157 → 0. Moved battles'
totals: kills 10 647 → 10 625, leaks 12 398 → 12 445, deaths 2 913 → 2 944. Unchanged: diy-040, diy-069.

End results that flip (one frame cascades; the causes as measured with the matches' battles instrumented):
- solo-NORMAL-1: won → lost in round 14 (passed 14 → 13, team LP 8 → 0): the Final Assault on 卢西恩 (pool 390 000) runs
  as before for two minutes (his 0.4 s wind-up a frame shorter), then the team's damage on the pool trails — 884 left
  when the overtime drain (from 300 game s) takes the last of the 17 LP at 334 s; before, it emptied at 318 s, 8 LP left.
- solo-HARD-1: LP 10 → 12 — round 8 lets one more through (26 → 25; rounds 11–12 even it out) and the Final Assault ends
  at 300.6 s, before the overtime drain (304.8 s and 2 LP before).
- solo-NORMAL-2: LP 9 → 25 — round 5 lets one more through (26 → 25, a coin less), the bot's shop path parts from round
  6 and its later board (突袭, 232 layers at round 13 against 94) clears rounds 12–13 with no leak (7 + 18 before).
- coop2-NORMAL-3: team LP 63 → 68 — ai_1's round-13 battle leaks 1 instead of 6 (its round-12 leaker now goes to 联防).
- coop2-ABYSS-10-boosted: team LP 583 → 630 — the Final Assault ends at 606.6 s (639.6) and the Hidden Core at 335.9 s
  (397.8): 47 LP less of overtime drain.
- coop2-NORMAL-14-standins: team LP 41 → 40 — ai_0's round-13 battle lets one 潜伏者 through (23 → 22).
- roster-016: cleared at 220.3 s → time-out at 230 s (主角阵营角色 leaks instead of falling; kills 101 → 105, leaks 26 → 22).
- roster-041: time-out → cleared at 189.0 s, perfect (the last 粉碎攻坚组长 falls inside the 190 s).
- bond-suntShip-high: cleared at 164.2 s → time-out at 190 s (kills 11 → 13, deaths 4 → 2).
- standin-04: perfect → 10 leaks (deaths 8 → 10; 萨卡兹大剑组长 / 萨卡兹穿刺手 / 澪 get through).
- diy-005 / diy-008 (艾丽妮 / 赫拉格): cleared at 225.3 s → time-out at 230 s (a last 宿主重装士兵 / 深池方阵指挥官 walking).
- diy-127 (白铁 / 酒神): time-out → cleared at 225.8 s (deaths 7 → 13, leaks 57 → 72).
- diy-131 (死芒): perfect → 1 leak (a 掠海漂移体).
The other matches keep their ends (solo-FUNNY-1/2, solo-HARD-2, solo-ABYSS-1/2, coop2-HARD-4, coop3-ABYSS-5,
coop4-FUNNY-6, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, solo-HARD-9-boosted, coop2-NORMAL-70-diy). PR #402's own
0.2.1 table (coop2-NORMAL-14-standins loss → win, solo-HARD-2 / solo-ABYSS-2 earlier exits) was measured before §27 and
does not repeat here.
2026-10-09 01:37:32 +08:00
sganggs 8c76309796 feedback7 R23-TOAST: 拟态物质 / 简易通讯机 / 寻呼模块 that grant nothing say why — 「{who}:卡池中已没有{name}」 (拟态物质 with 2 copies owned and the shared pool out: 1 elite + 2 normal 溯光星源 hold all 5 of the Ⅵ阶's copies) or 「{who}:没有可获得的同盟约干员」; the item is still destroyed (装备时销毁). GitHub #401, the owner's OK of 2026-10-09; en / ja / ko / zh-TW; golden: unchanged 2026-10-09 01:05:07 +08:00
sganggs 9bacd9ad45 feedback7: watch-bonds' "after the own battle" test gets a real window — the guest's enemies spawn at twice their times (new fastServer hook SP_SLOW_SPAWNS, startRealServer fast.slowSpawns), the host fields its 萨尔贡 five without 菲莱 (the planner puts a zero-range guard on the road since PR #339 and she held the host's battle to 109 s of 110), and 返回战场 tolerates the battles ending; measured: at the scheduled times a leaker's walk outlasted the host by 5–11 s in 0.2.1 and 0.2.2 alike (the 0.2.2 full pass lost 返回战场 to 联防) 2026-10-09 00:52:08 +08:00
sganggs e5e3f05bb2 feedback7: the co-op browser test counts an empty new shop slot as a chess slot (§27.52 — the slot a 调度中心 upgrade opens stays empty and shows an empty card until the next roll); found by the 0.2.2 full pass 2026-10-09 00:07:17 +08:00
sganggs ab7379e789 feedback7: §27.60 — an Esc pressed the moment the 自选 picker opens closes it (found by the full browser pass); the intro maps it 2026-10-08 23:50:54 +08:00
sganggs b2d33b1d41 feedback7: merge fb7-e2efix — an Esc pressed the moment the 自选 picker opens closes it (the listener comes with the picker's commit); the loadout / loadout-battle / teammate-loadout browser tests follow the 0.2.2 干员调配 (the row's quick skill / module choices, the default 练度 on the detail card, the detail back at the top before a click); golden: unchanged 2026-10-08 23:49:04 +08:00
sganggs 917e89240c feedback7 R22-E2EFIX: loadout e2e's pickChess waits for the detail to show the picked chess at the top before clicking in it; golden: unchanged
loadout.e2e 「局内数值 (desktop)…」 failed with 'Node is either not clickable or not an Element' on the 局内数值 精锐 tab
right after picking 信仰搅拌机. pickChess returned as soon as a stats cell was visible — the previous chess's — and the
detail resets its scroll for a new chess after paint (screens/loadout.js), so puppeteer scrolled the tab into view and
the reset then moved it below the viewport before the click (measured: scrollTop 366 → 0, the tab at y 1201 of 1080).
The 0.2.2 detail opens with 潜能与练度 and 特质, so 局内数值 / 模组 sit below the fold and need that scroll. pickChess now
waits for the detail's name and its body back at scrollTop 0. A player is not affected (the reset is the intended
behaviour); the assertions are unchanged.

- Tests (SP_E2E=1): loadout.e2e 11/11.
2026-10-08 23:44:02 +08:00
sganggs 267a826b58 feedback7 R22-E2EFIX: loadout-battle / teammate-loadout e2e choose 野鬃's S1 + 不装备 in the row's quick choices and expect the default 练度 on the detail card; golden: unchanged
The 0.2.2 干员调配 is one list: a row carries the skills and the elite's modules, and the detail (now opening with
潜能与练度 and 特质) has its module cards below the fold at 1920 × 1080, where the Client's real-mouse click (no
scrolling) found nothing clickable for `.lo-detail .lo-mod[data-module="none"]`. Both suites now tap the row's
`.lo-q--skill[data-skill="0"]` / `.lo-q--mod[data-module="none"]` and wait for the row (aria-pressed) and the detail
(is-on) to agree before the sync; what they assert is unchanged (m.private.loadout → b.start spec → the local sim
runs S1 without the module; the host's card of the guest's unit reads S1 已调配 / 未装备模组).
loadout-battle's detail card check then met 0.2.2's other change: the card's base ATK carries the player's 练度 — the
default 精英2 Lv.60, ×1.1 (576 for the record's 524, effects.json aceffect_char_4); the unit's def keeps 524.

- Tests (SP_E2E=1): loadout-battle 2/2, teammate-loadout 1/1.
2026-10-08 23:44:02 +08:00
sganggs b972c1fc9e feedback7 R22-E2EFIX: an Esc pressed the moment the 自选 picker opens closes it (its listener comes with the picker's commit); golden: unchanged
The 0.2.2 full browser pass failed diy.e2e 「5阶 / 6阶 slots: Esc cancels the picker…」 (GitHub #284's test) at the
wait for the picker to close after Esc. The picker does open on a real click (the suite's other three tests open it
with real mouse clicks; all green). The cause is a race in the UI: DiyPicker attached its keydown listener in a plain
effect, which runs a frame after the picker is in the DOM, while the 干员调配 overlay's own Esc handler skips Esc as soon
as `.diy-pick` exists — an Esc pressed in between was swallowed and the picker stayed open. Reproduced every time by
dispatching Esc right after the picker renders; on a loaded machine the test's own Esc hit the window. Now a layout
effect: the listener is live in the picker's first commit.

- Test: diy.e2e (the Esc test) adds that immediate Esc — it fails on the parent with the full pass's error and passes
  now; the whole suite 4/4 (SP_E2E=1), test/ui unit tests 699 pass.
2026-10-08 23:44:02 +08:00
sganggs 925f2d836d feedback7: §27.51 — two more scratch references replaced by plain wording 2026-10-08 22:49:06 +08:00
sganggs 115d4d66aa feedback7: DESIGN §27 (docs/history/0.2.2.md) assembled from the streams' drafts — 59 sections, the intro maps them by area, the owner's decisions of 2026-10-08; the DESIGN index row and version line
- Scratch repro references replaced by plain wording. docs-consistency / docs-paths pass.
2026-10-08 22:48:44 +08:00
sganggs 0ae2fac2b1 feedback7: merge fb7-unbalance — the official 失衡 (UNBALANCE) after a push / pull (the idea of PR #392 by @xcdoge, re-implemented from PRTS): no walking, attacking or skills while it lasts; pushes by the 推力-位移 time table, pulls by the force window (0.5 / 1 s), static bodies 0.1 s pushed / the window pulled; 弧光锋卫 bleeds 400 / 0.066 s while unbalanced; the client slide uses the same time
- 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.
2026-10-08 22:47:33 +08:00
sganggs 1db84d799d feedback7 R22-UNBAL: 失衡 review fixes — a pulled 静态刚体 is held for the pull's window, 弧光锋卫 bleeds 400 every 0.066 s while the state lasts, no enemy skill / blink / scripted move during it, a route APPEAR or a 逐火 重生 ends it, every push row holds its exact frames; golden: 32 move, 8 in outcomes (below)
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.
2026-10-08 22:42:26 +08:00
sganggs 0ced21d321 feedback7: merge fb7-loadout — per-operator 潜能 (1–6) and 练度 (未精英化 / 精英1 / 精英2 / 精英2 Lv.60) set in 干员调配, defaults 潜能 6 and 精英2 Lv.60 (the owner's decision of 2026-10-08); PR #301 ported as one aligned list; #398 the skill frame, #400 REA-Y's heal text
- 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).
2026-10-08 22:35:33 +08:00
sganggs 6629710632 feedback7 R22-LOADOUT: 圣约送葬人's REA-Y shows the trait's heal as 50 per enemy hit, as the battle heals (GitHub #400); golden: unchanged
#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.
2026-10-08 22:26:00 +08:00
sganggs 4d63e9d518 feedback7 R22-LOADOUT: the selected skill's frame is square and surrounds its icon again (GitHub #398, PR #399); golden: unchanged
#398 / PR #399 by @shadiaojumao-hyb: the blue frame of the selected skill (image_skill_select_outline, a 30 × 30 sprite
whose 3 px ring runs to its edges) rendered taller than wide, shifted left, with thin side lines and the icon poking
out. Cause, measured headless on the new layout (detail rows and quick buttons, 667–2560 px wide, DPR 1–3): theme.css
`img { max-width: 100% }` held the frame's width at the icon's padding box while `height: calc(100% + .1rem)` grew it
(36 × 40 px on a phone, 70 × 80 at 1920 px; its left edge at −.05rem, so its right edge fell inside the icon), and the
more specific `.lo-sicon img { object-fit: cover }` beat the frame's `fill`, cropping the side lines. The rule is now
`.lo-sicon .lo-sicon__outline` with `max-width: none`: square (40 × 40 / 80 × 80) and .05rem outside the icon on every
side, as designed. PR #399's own fix (inset 0, 100 %) also squares it but lays the ring over the icon's edge pixels;
its diagnosis is what this follows (credited in the CHANGELOG).

- Test: test/ui/loadout-rows.test.js — the frame rule's selector, size, max-width and fit; the global clamp it opts out of.
2026-10-08 22:21:08 +08:00
sganggs 9f36db32e8 feedback7 R22-LOADOUT: 干员调配's module card reads the module's talent changes at the operator's 潜能, as 局内数值 and the battle do; golden: unchanged
Grok's review of the branch (low): ModuleInfo read talentChanges from the data's module record — the full-potential one —
so at 潜能1 余's 闲云隐市 still read 「生命上限2%(+0.5%)」 while 局内数值 and the battle use 1.5 % (异客 +10 %(+2 %) vs
+8 %). ModuleInfo takes `potential` and composes the elite at it first (shared/potential.js atPotential; the module's
traitOverride does not depend on potential); the detail passes the operator's setting.

- Test: test/ui/loadout-rows.test.js — 余's module card at none / 潜能6 / 潜能1, 不装备's base 特性, the detail's prop.
2026-10-08 22:20:30 +08:00
sganggs 4f8f5d2acf feedback7 R22-LOADOUT: the 潜能与练度 note says an operator with nothing set fights at the default (满潜能, 精英2 Lv.60) and that 潜能1 / 未精英化 for an unowned 特许 operator is set by hand; golden: unchanged
Grok's review of the branch (medium): the note read 「未持有的特许干员按潜能1、未精英化」, as if the game applied it, while an
operator without a setting is fielded at 潜能 6 / 精英2 Lv.60 (the owner's 「默认满潜满加成」). It now says what
docs/PLAYING.md says: the default first, then the official rule (the player's own potential; the 自持有 bonus, a
separate multiplier; an unowned 特许 operator at 潜能1 without bonus officially — set it so by hand).

- i18n: the new note in the four packs; the replaced one (never released) removed.
- Test: test/ui/loadout-rows.test.js (the detail's section) reads the note.
2026-10-08 22:19:20 +08:00
sganggs 2de2feee3c feedback7: merge fb7-dobermann — 教鞭 is the official three-choice (PR #353 by @Sukvii, the owner's decision), the Final Assault / Hidden Core pay a 战术特训 card's perfect coins (guarded by b.progress leaksBy), the bots put zero-range heavy guards on the road first (PR #339 by @IceCodeNew, placement only)
- 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.
2026-10-08 22:11:49 +08:00
sganggs cee3784321 feedback7: merge fb7-late follow-ups — 画卷's copied items always arrive unequipped (PRTS 「获得的装备为未装备状态」; hand, then temp, destroyed with the toast when both are full), 度算浪波 breaks an HP-ratio tie by the latest deployment (PRTS)
- Golden union OK (roster-033 / roster-034 again); broad node --test 5602 (5600 pass, 0 fail, 2 skipped); lint 0 errors.
2026-10-08 22:04:15 +08:00
sganggs 1ab1276354 feedback7 dober: docs — a seat that left does not pick its open 教鞭 offer, it is eliminated and the offer dropped (META §2.5, design/match §6.6); golden: unchanged
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.
2026-10-08 22:01:37 +08:00
sganggs 66a228c64c feedback7 dober: a boss field's perfect-payout card is paid only where the client's result agrees with its own b.progress (b.progress.leaksBy splits the field's LP per player); golden: unchanged
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).
2026-10-08 22:01:26 +08:00
sganggs 03688c8297 feedback7 R22-LATE: 引星棘刺 S1 度算浪波 breaks an HP-ratio tie by the latest deployment; golden: roster-033, roster-034 move
PRTS 引星棘刺 技能1 度算浪波 备注: 「※优先选择生命比例最低>最晚部署的我方单位(不含装置职业单位)」. The kit took the ally
with the lowest HP ratio but broke a tie by more blocking, then the nearer one, then the unit id. A tie now goes to
the latest deployment (the highest deploySeq: a redeployed operator counts from its new deployment), the id last.
「不含装置职业单位」 was already met: alliesInGrid never returns a device.

Golden: roster-033 (a normal 引星棘刺 with S1) and roster-034 (an elite one) move — at the start every ally is at full
HP, so the first units now land on the latest deployed ally instead of the nearest one (033: killed 64 → 47, leaks
103 → 120; 034: 5_14_b damage taken 14691 → 11676 …).
Tests: test/content/kits_alt_t5.test.js +1 (two allies at 50 %: the later deployment, though farther, gets the unit;
a lower ratio still wins) — fails on the parent; all 引星棘刺 tests pass (9).
2026-10-08 21:57:48 +08:00
sganggs cdf2bd5c88 feedback7 R22-UNBAL: a pushed / pulled enemy is held in the official 失衡 (UNBALANCE) state — no walking, no normal attack — for the push's 位移时间 of its 受力等级 or the pull's force window; a 静态刚体 hit by a force gets the 0.1 s floor; the client's slide takes the same game time (idea from PR #392 by @xcdoge, re-implemented from the sources); golden: 69 scenarios move, 35 with kills / leaks / deaths / boss HP (below)
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.
2026-10-08 21:54:06 +08:00
sganggs 2492966c4f feedback7: merge fb7-late — #389 / PR #390 画卷 keeps the items to copy across a merge, #385 / PR #387 spectator bond details count the hand like the own popup, #394 / PR #395 Enter during facing selection follows the focus, #281 the attack range drawn on a tapped operator, #388 引星棘刺 S1 DEF stacks, PR #386 谢拉格 寒风 blows bottom-up (owner)
- Golden union OK (roster-033 / roster-034 moved on both sides: #298 and #388); broad node --test 5598 (5596 pass, 0 fail,
  2 skipped); lint 0 errors.
2026-10-08 21:54:03 +08:00
sganggs ac4231846f feedback7 R22-LATE: 画卷 never puts the copied items on the copy — they are gained unequipped into the hand / temp like any item; golden: unchanged
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.
2026-10-08 21:53:51 +08:00
leiming2333 7c56335c06 feedback7 R22-LATE: 谢拉格's 盟约寒风 blows a blizzard up the field with a stronger cold flash; 灵知 / 麦哲伦's own cold wind keeps the light flurry; golden: unchanged
GitHub PR #386 by @leiming2333 (this commit is substantially the PR: its blizzard particles and flash). The official
blizzard blows bottom-up (the owner's confirmation of 2026-10-08; community report 「谢拉格的效果特性不明显,暴风雪应该
从下向上刮」): snowflakes rise off the bottom edge on a crosswind (48, low 22; were 26 drifting down), 10 gust streaks
(low 4) race up through the field, and the cold flash goes from 0.35 / 0.8 s to 0.45 / 1 s. Counts, speeds and lives
are marked [ASSUMED] (a visual choice).

Added on top of the PR: the same 'chill' fx is also the coldWind of 灵知 S3 (失温症, at its start) and of 麦哲伦 S1's
pulse (every 3 s — 248 in one golden battle); the PR turned every one of them into the full-screen blizzard. Only the
bond's field-wide wind (the device emits no anchor unit) gets it now; an operator's own coldWind (`id` = the unit)
keeps the light flurry and the softer flash it had.

Tests: test/render/fxchill.test.js (2; the gust case fails on the parent). Display only: golden unchanged.
2026-10-08 21:45:09 +08:00
sganggs 1a67ecc35f feedback7 R22-LATE: 引星棘刺 S1 度算浪波 — two alchemy units on one ally add their DEF (+60 +60), as their heals already did; golden: roster-033, roster-034 move
GitHub #388 (claim 1). Every S1 alchemy unit applied its DEF buff under one shared key ('thorn2:bastion'), so a second
unit over the same ally replaced the first one's buff (two units: DEF +60, not +120; a normal and an elite: +75, not
+135) while both units' heals landed. PRTS 引星棘刺 技能1 备注: 「效果均可叠加」. Each alchemy unit now has its own
DEF buff key (assigned at its first tick), so the buffs add (aggregateMods sums defFlat) and each lapses with its own
unit. S3's 不叠加 debuff is untouched.

Golden: roster-033 (a normal 引星棘刺 with S1, pass 1) and roster-034 (an elite one, pass 1) move — one operator's
consecutive S1 units overlap for a few seconds (6 / 7 s + 3 s 心相 against a 7 / 6 SP cost) and now add their DEF on
an ally both cover (033: killed 46 → 64, leaks 121 → 103; 034: damage taken by 5_13_b 41498 → 14903 …).
Tests: test/content/kits_alt_t5.test.js (+1: normal + elite on one ally = +135, the longer unit's +75 stays alone,
nothing afterwards; fails on the parent with +75).
2026-10-08 21:45:09 +08:00
sganggs 8e2b981e8e feedback7 R22-LOADOUT: PLAYING.md explains 干员调配's list and the per-operator 潜能 / 练度 (default 潜能 6, 精英2 Lv.60); 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 练度.
2026-10-08 21:39:14 +08:00
sganggs dc336f24a1 feedback7 R22-LOADOUT: 干员调配's roster is one list — a row per operator with the quick skill / module choices and the 潜能 / 练度 selects in aligned columns — and the detail opens with 潜能与练度; golden: unchanged
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; 恢复默认).
2026-10-08 21:38:26 +08:00
sganggs 3969e4b012 feedback7: merge fb7-owner-sim (48a2d37e) — #316: 突袭 members jump to 隐匿 ground enemies too and one in range counts as 范围内有敌人 (the owner's decision of 2026-10-08)
- 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.
2026-10-08 21:31:47 +08:00
IceCodeNew 0ca2a9c21a feedback7 R22-LATE: an open detail card draws its field unit's current attack range — own board pieces in prep, any ally operator / summon in battle or on a scouted board; golden: unchanged
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).
2026-10-08 21:27:33 +08:00
sganggs 52f56319ac feedback7: merge fb7-gh-sim — PR #348 (take-the-highest chains), PR #293 (REA-Y in the engine), #329 ports (异客 13 tiles, 歌蕾蒂娅 net radius 1, 搅拌机 latest first, 大帝 uncapped), PR #365 (骨刺 forms + PRTS targeting), #298 (instant non-attack skills recast at most once per attack interval, attacks untouched), #364 (先兆者 shell 3×3), #220 (速射手 air first), 余 skips noStack bonds (owner)
- 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.
2026-10-08 21:27:04 +08:00
sganggs 48a2d37e9b feedback7 R22-OSIM: 突袭 members jump to 隐匿 ground enemies too, and one in range counts for 「范围内没有敌人」 (the owner's decision, GitHub #316); golden: 5 match scenarios move
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).
2026-10-08 21:21:44 +08:00
sganggs 31fee55c6c feedback7: merge fb7-fields — PR #337 boss-field portals teleport (route-end portals: hidden 3 s, out of [5,10], leak at the side's door), PR #347 碎铳之簧 chases the full 5 s, #370 耀骑士临光's redeploy reduction floored at 0, #375 the water-map patrol takes the official upper road
- 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.
2026-10-08 21:20:37 +08:00
sganggs a1bc37ed90 feedback7 R22-LATE: while a facing is chosen, Enter follows the focus — ✕ cancels, any other button does nothing, the wheel confirms; golden: unchanged
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).
2026-10-08 21:20:27 +08:00
sganggs f5595a7a31 feedback7: merge fb7-owner-sim — community reports via the owner: 白铁's 铁钳号 opens only the skills that act on it (圣约送葬人 spends and stacks), 伊内丝 S3 after a 突袭 jump, 引星棘刺 alchemy units as HP regen (不屈者 / 禁疗), 不可对空 effects spare flying allies (炎佑), 望's 跟子 at battle start, 香槟炸弹 unhealable / no HP loss (owner decision) and not set off by unrevealed 隐匿 or sleeping enemies
- 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.
2026-10-08 21:14:27 +08:00
sganggs 2df1cf15bf feedback7 R22-LATE: a watched teammate's bond popup counts their bench like their own popup — hand operators owned (and counted for 投资人 / 远见 / 奇迹), temp ones never, neither 在场; golden: unchanged
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).
2026-10-08 21:11:25 +08:00
sganggs 378adfcac1 feedback7 R22-GHSIM: test/ui/feedback5-result-bonds.test.js's header no longer lists 余 among the strategies that add layers to the noStack bonds (38d4c3e4, the owner's decision of 2026-10-08); golden: unchanged 2026-10-08 21:06:45 +08:00
sganggs db29acf907 feedback7 R22-GHSIM: #298 rework — the one-interval gap of an instant skill with no attack of its own sits on the skill (nextCastAt), not on the operator's attack cooldown: its casts no longer replace the attacks / heals that were due; golden: back to 2 moves against the code before #298 (roster-033, roster-034)
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.
2026-10-08 21:06:45 +08:00
farridge 519f04e2d5 feedback7 R22-LOADOUT: 干员调配's detail starts with the operator's 特质 (the in-match card's block), and the compact skill tags of the quick choices; golden: unchanged
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.
2026-10-08 21:06:30 +08:00
sganggs 1205336a27 feedback7 R22-FIELDS: an enemy whose route ends on a corner portal stays hidden 3 s inside it before it comes out of [5,10], like the explicit crossings of the same tiles (review of the #337 commit); golden: 7 fields, 4 matches — solo-NORMAL-1's Hidden Core still fails
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.
2026-10-08 21:05:18 +08:00
Xinglan233 19c7b26850 feedback7 R22-LATE: a 画卷 copy that completes a three-copy merge copies the target's equipment again; golden: unchanged
GitHub #389 / PR #390 by @Xinglan233 (this commit is substantially the PR: the snapshot and its test file, ported).
onArt read target.items after grantChess; a gain that completes a merge runs _mergeChess first, which consumes the
target and empties its items, so the copy loop ran on an empty list (two base 维式重锤 never became the +25% one).
The item ids are now recorded before the gain.

Added on top of the PR: after a promotion the copied items are never equipped onto the elite (an already-advanced
item used to go straight back onto it via equipDirect). The promotion unequips the copies (PRTS 帮助 自动卸除 on
合并) and the 画卷 note says the obtained equipment arrives unequipped, so they wait in the hand; a copied normal item
merges with the returned original as before. The non-promotion path is unchanged.

Tests: test/match/scroll-copy-merge.test.js (8; 4 fail on the parent, the advanced-item case also fails with the
PR's snapshot alone). Golden unchanged (no scenario casts 画卷 into a promotion).
2026-10-08 21:04:58 +08:00
sganggs 7a0937e3dd feedback7 R22-LOADOUT: the browser stores, syncs and shows each operator's 潜能 / 练度 (cards at the player's settings); golden: unchanged
- ui/loadoutModel.js: `ops` beside the entries (sp.pref.loadout { v, entries, ops }; an older stored loadout has none =
  潜能 6, 精英2 Lv.60): parseStoredOps, opsOf / setOps / resetOps (defaults never stored), sanitizeOps (only what the
  server keeps: the roster and the 自选 owned pool); 导出 carries `ops`, 导入 replaces the settings when the payload has
  them (an older export leaves them alone; settings alone import); the 已调整 count and 仅看已调整 count them.
- ui/loadoutSync.js: the store's `ops` (setOpsMap persists both); the loadout sync watches entries and ops and sends
  room.loadout { entries, ops } (the ops sanitised against chess.json + backups.json; nothing set: no download);
  openLoadout loads effects.json (the 练度 multipliers).
- Cards: gameLogic chessLoadout(chess, loadout, getChess, { ops | cultivation, effects }) composes the record at the
  operator's potential and multiplies its stats by the 练度 tier (the player's m.private.ops — shop / hand / board cards
  and the detail panel; a teammate's unit its UnitInfo potential / cultivate: unitCultivation; a teammate's bond-popup
  card the defaults, never the viewer's settings; a stand-in neither); a 自选 card is composed at its pick's potential
  (ownDiyPick from m.private.ops, unitPick from UnitInfo).
- Tests: test/ui/potential-ui.test.js (new, 7): storage, helpers, 导出 / 导入, the 已调整 count, the sync (sanitised ops,
  settings-only edits, no download when none), the cards (刺玫 413 / 17 at 潜能1, ×1.05 at 精英1, a teammate's unit, a
  stand-in), 自选 cards at 潜能1 (凯尔希 392 / 20) and the 练度 on top; test/ui/loadout.test.js expects the envelope's
  `ops` and room.loadout's `ops: {}`.
2026-10-08 21:01:09 +08:00
IceCodeNew e63214571d feedback7 dober: the bots plan a zero-range heavy guard (range 0-1: only its own tile) on a free enemy-road tile first (PR #339 by @IceCodeNew, that part only); golden: 9 of 18 matches move, no result flips
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.
2026-10-08 20:58:20 +08:00