New on feedback4 since the last merge: W4D (bbe59b9, merged as7ee815e) and the 0.1.4 version commit (30e7ace). feedback5 keeps its own package.json / package-lock.json / APP_VERSION / README / DEPLOY version lines (0.2.0 sets its own version later) and takes feedback4's CHANGELOG 0.1.4 entry, with W4D's line in it. - Port (PRTS 异常效果 SLEEPING 沉睡 = 无法行动+无敌+不可阻挡; GitHub #140; the owner's decision of 2026-10-06): `_checkBlock` refuses `flags.sleep` (battle/blocking.js) and `applyStatus` releases an enemy that falls asleep (battle/status.js). Battle.js stays feedback5's façade, and tier3.js stays deleted (the 瑕光 comment is in her kits/ops file). ai.js: WE1 (89f730d) cuts a swing before enemyAttack on a stun / freeze / sleep. The cannot-act branch is still there, and a plain-buff `flags.sleep` still makes the enemy `stun` (units.js), so the plain-buff release stays in that branch, after the cut, with its comment rewritten for the new order. - New on feedback5: 酒神's 迷狂牢笼 (kits/ops/op-phatm2.js cageBlock, O22) duplicated the old three-flag 不可阻挡 check. It now skips a sleeper too. No golden scenario spawns a 牢笼 over a sleeper: the corpus is identical with that line reverted. 予愿安洁莉娜's S3 helper is unchanged, since canTargetEnemy already drops sleepers. - Readers that follow without code: blockedTargets (blocked-first targeting, the DEFAULT trigger, hitAllBlocked), the 要塞 branch of acquireTargets (u.blocking), “庞贝”'s clock (blockedBy), the snapshot's UF.BLOCKED and the client's block marker (render/units.js). 死芒 S2 折朽 (自选) lets go a linked enemy that her 悲叹的仆役 held. - Docs: DESIGN §24.9, its §24 intro entry, the §5.5 Blocking sentence (on feedback5's 阻挡半径倍率 text) and §22.8's "(since §24.9)", all as on feedback4; SIM.md as on feedback4 (the plain-buff sentence merged with feedback5's noNewBlock / undying text). - Golden: a probe (counting successful sleeps on a blocked enemy, plain-buff sleeps and 牢笼 spawns over sleepers) ran over all 283 scenarios. Every scenario that moved sleeps blocked enemies; the counts are in brackets: roster-003 (小满 S2 [5]: same fight, blocked 27 → 29, snapshot hashes); roster-005 (elite 小满 S2 [3]: the 140 s timeout with 83 kills and 1 leak, where it used to clear at 135.43 s with 84); roster-013 (缇缇 S3 [11]: same kills, blocked 22 → 33); roster-032 (缇缇 S1 [10]: 71 kills instead of 68, 8 leaks instead of 11, 4 knock-outs instead of 8, 萨尔贡 layers 41 → 35); roster-038 (elite 缇缇 S2 [1]: ends at 197.40 s instead of 197.27 s); roster-039 (瑕光 S2 normal + elite [31 + 12]: same kills and counted leaks, the 窃贼 gets away, blocked 53 → 87); roster-042 (缇缇 S2 [3]: ends at 168.77 s instead of 166.93 s); bond-maniShip-high (小满 [1]: blocked 29 → 30, snapshot hashes); solo-FUNNY-1 (ai_0's 缇缇 [3]: only ai_0's damage dealt); coop4-FUNNY-6 (ai_0's 缇缇 [15 + 56]: from round 9 ai_0's 精准 / 萨尔贡 layers and boss damage move, ai_1 loses no LP from round 11, team LP 124 → 130); coop4-ABYSS-7 (ai_2's 小满 [18]: ai_2 one kill fewer, ai_0 one more LP lost in round 9); coop2-NORMAL-8-serverrun (ai_1's 缇缇 [83]: only ai_1's damage dealt; WE1 had already turned this match into a loss); coop2-NORMAL-70-diy (the 自选 seat h_0's 缇缇 [171], ai_0's 缇缇 [74] and 小满 [19 + 21]: h_0's layers move from round 4, then its AI 托管 buys differently (boards from round 7); ai_0 246 kills instead of 233, same end); diy-131 (死芒 S2 at tiers 5 and 6 [1 + 1]: snapshot hashes only); diy-132 (elite 死芒 S2 [2]: ends at 199.50 s instead of 198.13 s, same kills). One scenario sleeps a blocked enemy and did not move: coop2-HARD-4 [1]. ai_1's 小满 lets go the enemy it holds; another enemy takes the slot; the sleeper wakes with no room and is blocked further on by 2_16_a. The match digest has no snapshot hashes, and its kills, leaks, damage and funds are identical. fields and standins did not move. Most outcomes differ from feedback4's list because feedback5's baselines differ (WE1's damage frame, etc.). - Tests: test/sim/feedback4-sleep-unblock.test.js (from feedback4) passes 5/5 on the split layout. Every case fails with the three sim hunks reverted, and case 3 (plain buff) fails with only the ai.js release reverted. Targeted sim tests pass 184/184 (blocking, block targets, enemy swing, feedback2-enemy-attack-stand, combat / statuses, robustness, stun SP, professions, healers, fidelity …). Content tests plus kits_layout and docs-consistency pass 658/658 (kits t1t2 / t3 / t5 and alt t2 / t3 / t5 for 小满 / 瑕光 / 缇缇, 酒神, 死芒, garrisons, bosses, items). Stealth / liftoff / enemies pass 36/36. npm run golden: all 283 match. GOLDEN_FULL=1 test/golden.test.js passes 9/9 (no stored engine error). lint 0 errors, check:imports and typecheck clean.
Golden results
The simulation is deterministic: the same seed and inputs give the same battle and the same match, bit for bit. These files record the exact outcome of a fixed corpus of seeded scenarios, so a change that alters gameplay — even one meant to be a pure refactor that only reorders random draws, hook order or iteration order — fails loudly instead of slipping through.
tools/golden.mjsbuilds the corpus fromdata/*.json(fixed order, fixed seeds) and reduces every scenario to a digest. Its header documents the families and the digest fields.test/golden/<family>.jsonhold the digests (roster,bonds,fields,matches,standins,diy).test/golden.test.jsrecomputes them and compares. On a mismatch it prints the scenario, the field and old → new.
The corpus
| family | what runs |
|---|---|
roster |
49 battles: every visible chess record (normal and elite) with every selectable skill and module (DESIGN §16), 12 operators per battle on a real stage (all 11 in turn) against the round's real wave three times over, every non-leader enemy kind of data/enemies.json as extra spawns (half of them bounties), placeable summons on the board, every equipment item, band, battle-side 机变 card and stage map card in turn, bonds from the board |
bonds |
46 battles: every bond at its activation threshold (1 layer) and at its top tier (999 layers) |
fields |
22 battles: every Final Assault / Hidden Core leader on a pair and a solo template (shared pool, 200 s cap) and the 联防 field with 1 and 2 helpers on its own map (carried HP / SP, a knocked-out operator, two leakers' enemies) |
matches |
18 matches to the end in virtual time: 16 bot-only (solo 标准 / 险境 / 绝境 / 终极 × 2 seeds, co-op 2 / 3 / 4, one server-run combat match, two runs boosted to the Hidden Core), one co-op match whose human seat (AI 托管, offline) does not own 7 NORMAL chess — they fight as their 补位 stand-ins (its digest lists them per round, standIns) — and one whose human seat slots 自选 picks (推进之王 and prototypes) with its 调度中心 at level 5 from the first prep: its own shop draws them and its AI fields 推进之王 (its digest lists per round the 自选 shop draws and the fielded pieces, diy) |
standins |
10 battles: every NORMAL chess record (normal and elite, 110) fielded as its 补位 stand-in (standIn: true: the stand-in's body, backup skill / module and kit — all 17 stand-ins, every skill a chess names for them), 12 per battle by strength band, laid out by the stand-in's position on a real stage, against the round's real wave three times over plus 8 ground enemy kinds (melee stand-ins always meet an enemy), an item each, bonds from the board |
diy |
battles of 自选 pieces (a DIY slot with its diy pick, shared/diy.js): every owned 6★ with an operator kit (kits/index.js OPERATOR_KITS) in each form of tiers 5 and 6 (normal; elite with no module and with each module) under each skill, then every prototype pick with a kit at its locked selection, normal and elite, 12 per battle against the round's real wave; a new operator kit adds its scenarios (the battle count grows with the kits) |
Battles go through the production BattleSpec path (server/sim/spec.js, as browsers and the server's headless fields
run them); matches construct Match directly with a VirtualScheduler. Every option is explicit, so a change to a
test-harness default never moves a digest.
What a digest ignores on purpose (it does not affect gameplay): object key order, engine unit ids (snapshot ids are renumbered by first appearance, units are named by board uid or def id), board piece uids, the order of client events within a tick (they are counted, not hashed) and wall-clock time. Damage / healing sums are rounded to integers and HP to 2 decimals.
npm test runs the fast subset (the scenarios marked "fast": true: every chess record with its default loadout,
every stage and every non-leader enemy kind, every bond at its top tier, six fields, seven matches, the five stand-in
battles of the normal records, the first 自选 battle of the operator kits and of the prototypes — 62 of the 151). GOLDEN_FULL=1 checks everything.
Workflow
GOLDEN_FULL=1 node --test test/golden.test.js # the whole corpus (≈ 20 s with 4 worker threads)
node --test test/golden.test.js # the fast subset
npm run golden # the same comparison from the tool (all families, a short report)
npm run golden:update # recompute and rewrite test/golden/*.json
node tools/golden.mjs --twice # determinism: the corpus twice in one process, the second pass reversed
- A refactor commit must never change these files. Run
GOLDEN_FULL=1 node --test test/golden.test.jsbefore committing it. If a digest moves, the refactor changed behaviour: find out why (the first differing value ofsnapssays when two runs parted, in 10-game-second steps) instead of updating the files. - An intended gameplay change (a fix, new content, a data rebuild): run
npm run golden:updateand commit the changed files together with the change, saying in the commit message which scenarios moved and why. - Reviewing the diff: every scenario is a block of its own and every unit / enemy / round is one line, so a diff
shows which scenarios moved and which values changed. A change confined to the scenarios that contain the touched
operator, enemy or rule is expected; a change everywhere (every
rngDraws, everysnaps) means something global moved — the RNG draw order, the tick order, a shared rule. - Adding to the corpus (a new family, more scenarios, a new digest field) changes the files by design: do it in a commit of its own, never together with a refactor.
- A new engine hook, event or chess record does not change the digests of the existing scenarios by itself — the
hook list is frozen in the tool and the corpus is regenerated only on
golden:update— but new data records do join the corpus there (the roster family enumeratesdata/chess.json).