feedback7 R22-HUD: a skill cue that reaches the client before its unit is held and sounds once when the unit is tracked — the first cast of a deploy-time skill was silent (PR #292 by @LimitlessHPPK, without handing the whole early buffer to the sound); golden: unchanged
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).
This commit is contained in:
1 parent
030d0a2938
commit
3484178e2e
6 files changed
+170
-4
No files matched your search
@@ -17,6 +17,8 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createBattleRunner, keepsState, compactHeld, HELD_MAX, EV_SLICE } from '../../public/js/battle/runner.js';
|
||||
import { SnapshotBuffer } from '../../public/js/render/interp.js';
|
||||
import { keepEarly, audioEarly } from '../../public/js/screens/game/early.js';
|
||||
import { AudioManager } from '../../public/js/audio.js';
|
||||
import { createStore, initialState } from '../../public/js/store.js';
|
||||
import * as specMod from '../../server/sim/spec.js';
|
||||
import { DataSource } from '../../server/sim/simdata.js';
|
||||
@@ -243,6 +245,56 @@ test('screens/game.js buffers the form fx with the state-bearing events it repla
|
||||
assert.match(src, /lastFieldRef\.current = field\.fieldId;\s+reentryRef\.current = null;/);
|
||||
});
|
||||
|
||||
// GitHub PR #292 (by @LimitlessHPPK) wanted the whole pre-entry buffer handed to the sound as well, so that a deploy-tick cast
|
||||
// ahead of its unit's spawn would be heard. The review kept the hold of such a cast (audio.js `pendingSkill`, which the live
|
||||
// path needs: `onEv` already forwards every list) and refused the replay: the buffer is every spawn, die, deploy, status and
|
||||
// skill since the field's m.field (up to 1500 of them), so the sound would play the field's old deaths, deploy lines and casts
|
||||
// all at once on entering.
|
||||
test('entering a field late: the view replays every buffered state event, the sound only the spawn tuples that teach it who is there', () => {
|
||||
const src = readFileSync(path.join(ROOT, 'public/js/screens/game.js'), 'utf8');
|
||||
assert.match(src, /view\.pushEvents\(early\);/, 'the view gets the buffered list');
|
||||
assert.match(src, /audio\.handleBattleEvents\(audioEarly\(early\)\);/, 'the sound gets audioEarly of it');
|
||||
assert.ok(!/audio\.handleBattleEvents\(early\)/.test(src), 'never the whole list');
|
||||
const stream = [
|
||||
['atk', 1, 2, 'none'], // stale cosmetics: dropped by keepEarly
|
||||
['dmg', 2, 40, 'phys'],
|
||||
['skill', 1, 1], // a deploy-tick cast, ahead of its unit's spawn
|
||||
['spawn', { id: 1, side: 'ally', kind: 'op', spine: 'char_a', skillIndex: 2 }],
|
||||
['spawn', { id: 2, side: 'enemy', kind: 'enemy', spine: 'enemy_x' }],
|
||||
['deploy', 1],
|
||||
['skill', 1, 0],
|
||||
['fx', 'form', 2, 3, { id: 2, form: 'translator_youling' }],
|
||||
['die', 2, 'killed'],
|
||||
];
|
||||
const early = stream.filter(keepEarly);
|
||||
assert.deepEqual(early.map((e) => e[0]), ['skill', 'spawn', 'spawn', 'deploy', 'skill', 'fx', 'die'], 'the pre-entry buffer keeps every state-bearing kind');
|
||||
assert.deepEqual(audioEarly(early).map((e) => e[0]), ['spawn', 'spawn'], 'the sound is told who is on the field, nothing else');
|
||||
assert.deepEqual(audioEarly(null), [], 'no buffer, nothing');
|
||||
// through a real AudioManager: the replay makes no sound at all (no old death, deploy line or cast) but tracks the units,
|
||||
// so the first live cast after entering sounds
|
||||
const vm = { audio: { sfx: { ui: {}, battle: {}, units: { char_a: { skill: '/s/a_skill.mp3', born: '/s/a_born.mp3' }, enemy_x: { die: '/s/x_die.mp3' } } }, voice: {} } };
|
||||
const a = new AudioManager({ win: null, getManifest: () => vm });
|
||||
a.ctx = {};
|
||||
const played = [];
|
||||
a._play = (url) => { played.push(url); };
|
||||
a.voice = () => { played.push('voice'); return true; };
|
||||
a.setFieldUnits([]);
|
||||
a.handleBattleEvents(audioEarly(early));
|
||||
assert.deepEqual(played, [], 'the replay is silent');
|
||||
assert.equal(a.units.size, 2, 'and has taught the sound both units');
|
||||
a.handleBattleEvents([['skill', 1, 1]]);
|
||||
assert.ok(played.includes('/s/a_skill.mp3'), 'the next live cast of a known unit sounds');
|
||||
// (the contrast: the whole list would have played the buffered death, the deploy and the cast on entering)
|
||||
const b = new AudioManager({ win: null, getManifest: () => vm });
|
||||
b.ctx = {};
|
||||
const heard = [];
|
||||
b._play = (url) => { heard.push(url); };
|
||||
b.voice = () => true;
|
||||
b.setFieldUnits([]);
|
||||
b.handleBattleEvents(early);
|
||||
assert.ok(heard.length >= 2, `the whole list would sound ${heard.length} old cues at once`);
|
||||
});
|
||||
|
||||
/**
|
||||
* The view's side too (render/app.js): the runner's frames go into the render engine's SnapshotBuffer, whose clock
|
||||
* follows the snapshots, and processEvents takes the due events with the 1.5 game s stale-cosmetic window — a form fx
|
||||
|
||||
Reference in new issue
Block a user