Seen (Grok's review, reproduced here with the real sim): the sim's deploy emits a deployment-activated skill's
['skill', id, 1] before the unit's ['spawn', unitInfo] (宴: three events earlier) and audio.js dropped a skill event of an
unknown unit, so that one cast made no sound while every later one did.
- audio.js: pendingSkill — a 'skill' ON for an unknown unit is held (one per unit, 64 at most, the oldest go first);
_track plays it once when the unit's spawn arrives; a skill OFF is no cast and is not held; setFieldUnits drops the holds.
Changed from the PR: setFieldUnits clears the holds BEFORE it tracks the new field's units (the PR cleared after, so a hold
left over from the previous field would have sounded for a unit of the new field that shares its id — ids are per battle).
- screens/game.js: NOT the PR's change. The pre-entry buffer (keepEarly: every spawn, die, deploy, status and skill since the
field's m.field, up to 1500 of them) is handed to the sound as its 'spawn' tuples only (new early.js audioEarly): the whole
list would play every old death, deploy line and cast at once on entering. The live path (onEv) already forwards the full
list, so the hold alone fixes the live silent first cast; a deploy-tick cast that falls into the short pre-entry gap
stays unheard (a deliberate gap — replaying "only the casts before their spawn" would still sound a minute-old cast when a
field was buffered while another one was on screen).
- Tests: test/ui/audio.test.js (3: the held cast in one batch and across batches, once, no hold for a skill OFF; a hold does
not outlive its field and is bounded; 宴's real deploy: the sim's cast comes before the spawn and sounds exactly once — all
fail on the parent) and test/match/feedback1-runner-forms.test.js (the audio gets the spawn tuples only: silent replay that
still teaches the units, the next live cast sounds; the whole list would have sounded old cues). Docs: design/client.md.
- Golden: unchanged (client audio only).
The contributor's code (server/sim/content/traitMods.js, the battle/players.js _setupUnit wiring after kit.install, the
crowdAspd removal from shared/tier5.js and the two kits, test/sim/trait_attack_speed.test.js), applied from PR #293, so
they are the author of this commit. Maintainer additions: the coverage lock also scans the 补位 stand-ins (266) and every
legal 自选 combination (pool operator × slot form × skill × non-other-mode module) — none gains the rule; 黑键 / 维伊
MSC-Y 「拥有已储存的攻击能量时,攻击速度+30」 (their 自选 kits own it) is listed as kit-owned instead of 'unknown'; the
comment's history note named a commit this repository does not have, and its line-number references are now names;
docs/ARCHITECTURE.md and kits/README.md name the file.
Official: battle_equip_table uniequip_003_excu2 (display trait + hidden talent attack_speed 12, trigger_cnt[equip] 2) and
uniequip_003_etlchi (trait attack_speed 12, cnt 2), both 「攻击范围内存在2名及以上敌人时攻击速度+12」; 仇白's
uniequip_003_qiubai carries the same sentence on her named talent, which her kit keeps reading (not seen by the engine).
Before: shared/tier5.js crowdAspd polled every 0.2 s and gave a 0.3 s buff — measured +12 0.167 s after the second enemy
arrived and 0.267 s after it died (scratchpad repro); now one tick each. Golden: unchanged (no sampled snapshot moves).
Ported only the dual-stack listen from PR #291 (the change accepted as #188). With HOST unset the server binds :: and, if that fails with EAFNOSUPPORT, EADDRNOTAVAIL or EINVAL, retries 0.0.0.0. An explicit HOST, including 0.0.0.0 and 127.0.0.1, is tried once. The boot banner and doctor list shareable IPv6 addresses in brackets, skip fe80::, and keep one URL per /64. An explicit IPv6 literal in the local URL is bracketed. The Windows service listing no longer drops those bracketed lines.
Not ported: /dev/grant, the settings-default migration, and the firewall scripts. The Docker image stays on 0.0.0.0; the alpine fallback was not booted.
Tests: node --test test/ipv6-bind.test.js test/doctor.test.js test/ci.test.js — 20 pass, 0 fail (8 bind, 4 doctor, 8 ci).