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).