203 lines
7.3 KiB
JavaScript
203 lines
7.3 KiB
JavaScript
/**
|
|
* SFX voice test (dev tool, run with Node — no browser needed):
|
|
*
|
|
* node dev/sfx.test.mjs
|
|
*
|
|
* Exercises the shared SFX voice (js/utils/Sfx.js) with a stub scene:
|
|
* playSfxOn() guard rails (master switch off, missing sound manager,
|
|
* asset not in the audio cache, the happy path, extra play options like
|
|
* loop) and the stop/status helpers (stopSfxOn / sfxPlayingOn). Also
|
|
* checks playSfxOneOfOn() (the one-of-a-set voice: a thunder strike,
|
|
* a solar flare) and that the sfx.json keys follow the play-name
|
|
* convention (sfx_<key> cache key) and point at files that exist.
|
|
*/
|
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import fs from 'node:fs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const SFX = JSON.parse(fs.readFileSync(join(__dirname, '../data/sfx.json'), 'utf8'));
|
|
|
|
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
|
|
config.init({ sfx: { ...SFX } });
|
|
|
|
const { playSfxOn, playSfxOneOfOn, stopSfxOn, sfxPlayingOn } = await import(pathToFileURL(join(__dirname, '../js/utils/Sfx.js')).href);
|
|
|
|
let failures = 0;
|
|
const check = (label, cond) => {
|
|
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
|
|
if (!cond) failures++;
|
|
};
|
|
|
|
/** A stub scene: sound manager + audio cache, recording play() calls. */
|
|
const makeScene = (o = {}) => {
|
|
const calls = [];
|
|
const stops = [];
|
|
let playing = false;
|
|
return {
|
|
calls,
|
|
stops,
|
|
sound:
|
|
o.sound === false
|
|
? null
|
|
: {
|
|
play: (key, opts) => {
|
|
calls.push({ key, opts });
|
|
playing = true;
|
|
},
|
|
stopByKey: (key) => {
|
|
stops.push(key);
|
|
playing = false;
|
|
},
|
|
isPlaying: (key) => playing && key === calls[calls.length - 1]?.key,
|
|
},
|
|
cache: o.noCache
|
|
? undefined
|
|
: { audio: o.noAudio ? undefined : { has: (k) => (o.inCache === false ? false : true) } },
|
|
};
|
|
};
|
|
|
|
// 1) Happy path: plays the sfx_ key at sfx.volume.
|
|
{
|
|
const scene = makeScene();
|
|
playSfxOn(scene, 'ui_click');
|
|
check('plays sfx_ui_click at sfx.volume',
|
|
scene.calls.length === 1 &&
|
|
scene.calls[0].key === 'sfx_ui_click' &&
|
|
scene.calls[0].opts.volume === SFX.volume);
|
|
}
|
|
|
|
// 2) Master switch off → silent, no throw.
|
|
{
|
|
const scene = makeScene();
|
|
config.init({ sfx: { ...SFX, enabled: false } });
|
|
playSfxOn(scene, 'ui_hover');
|
|
check('sfx.enabled=false → nothing plays', scene.calls.length === 0);
|
|
config.init({ sfx: { ...SFX } });
|
|
}
|
|
|
|
// 3) No sound manager (headless) → silent, no throw.
|
|
{
|
|
const scene = makeScene({ sound: false });
|
|
playSfxOn(scene, 'ui_click');
|
|
check('no sound manager → nothing plays', scene.calls.length === 0);
|
|
}
|
|
|
|
// 4) Asset not in the audio cache → silent, no throw (v4 play() would throw).
|
|
{
|
|
const scene = makeScene({ inCache: false });
|
|
playSfxOn(scene, 'ui_hover');
|
|
check('asset missing from cache → nothing plays', scene.calls.length === 0);
|
|
}
|
|
|
|
// 5) Cache object without an audio section → still plays (defensive guard).
|
|
{
|
|
const scene = makeScene({ noAudio: true });
|
|
playSfxOn(scene, 'ui_click');
|
|
check('cache without audio section → plays', scene.calls.length === 1);
|
|
}
|
|
|
|
// 6) data/sfx.json carries the UI ticks + the window whoosh + the close tick, pointing at real files.
|
|
{
|
|
const ok = ['ui_hover', 'ui_click', 'ui_window', 'ui_close'].every((k) => {
|
|
const p = SFX[k];
|
|
return typeof p === 'string' && fs.existsSync(join(__dirname, '..', p));
|
|
});
|
|
check('sfx.json ui_hover/ui_click/ui_window/ui_close point at files that exist', ok);
|
|
}
|
|
|
|
// 7) Extra play options (the mining hum's loop) pass through after the
|
|
// configured volume.
|
|
{
|
|
const scene = makeScene();
|
|
playSfxOn(scene, 'mining_loop', { loop: true });
|
|
check('playSfxOn passes extra options (loop) through with the sfx.volume default',
|
|
scene.calls.length === 1 &&
|
|
scene.calls[0].key === 'sfx_mining_loop' &&
|
|
scene.calls[0].opts.loop === true &&
|
|
scene.calls[0].opts.volume === SFX.volume);
|
|
}
|
|
|
|
// 8) stopSfxOn stops by the sfx_ cache key (v4 has no sound.stop()).
|
|
{
|
|
const scene = makeScene();
|
|
playSfxOn(scene, 'mining_loop', { loop: true });
|
|
stopSfxOn(scene, 'mining_loop');
|
|
check('stopSfxOn stops the sfx_ key', scene.stops.length === 1 && scene.stops[0] === 'sfx_mining_loop');
|
|
|
|
const noMgr = makeScene({ sound: false });
|
|
let threw = false;
|
|
try {
|
|
stopSfxOn(noMgr, 'mining_loop');
|
|
} catch (e) {
|
|
threw = true;
|
|
}
|
|
check('stopSfxOn with no sound manager → no throw', !threw);
|
|
}
|
|
|
|
// 9) sfxPlayingOn mirrors the manager's isPlaying (false when unsure).
|
|
{
|
|
const scene = makeScene();
|
|
check('sfxPlayingOn is false before anything plays', sfxPlayingOn(scene, 'mining_loop') === false);
|
|
playSfxOn(scene, 'mining_loop', { loop: true });
|
|
check('sfxPlayingOn is true while it plays', sfxPlayingOn(scene, 'mining_loop') === true);
|
|
stopSfxOn(scene, 'mining_loop');
|
|
check('sfxPlayingOn is false after stop', sfxPlayingOn(scene, 'mining_loop') === false);
|
|
|
|
const noMgr = makeScene({ sound: false });
|
|
check('sfxPlayingOn with no sound manager → false (no throw)', sfxPlayingOn(noMgr, 'mining_loop') === false);
|
|
}
|
|
|
|
// 10) The mining_loop key points at a real file.
|
|
{
|
|
const p = SFX.mining_loop;
|
|
check('sfx.json mining_loop points at a file that exists',
|
|
typeof p === 'string' && fs.existsSync(join(__dirname, '..', p)));
|
|
}
|
|
|
|
// 11) EVERY sfx.json asset key points at a file that exists (the scene
|
|
// load list trusts the data; a typo would be a silent dead voice).
|
|
{
|
|
const keys = Object.entries(SFX)
|
|
.filter(([k, v]) => k !== '_comment' && typeof v === 'string');
|
|
const ok = keys.length > 0 && keys.every(([, v]) => fs.existsSync(join(__dirname, '..', v)));
|
|
check(`every sfx.json asset key (n=${keys.length}) points at a file that exists`, ok);
|
|
}
|
|
|
|
// 12) The new effect voices (thunder / solar) point at real files.
|
|
{
|
|
const ok = ['thunder_01', 'thunder_02', 'thunder_03', 'thunder_04',
|
|
'solar_buzz', 'solar_flare_01', 'solar_flare_02'].every((k) =>
|
|
typeof SFX[k] === 'string' && fs.existsSync(join(__dirname, '..', SFX[k])));
|
|
check('sfx.json thunder_01…04 + solar_buzz + solar_flare_01/02 point at files that exist', ok);
|
|
}
|
|
|
|
// 13) playSfxOneOfOn: exactly ONE member of the set plays, always a valid
|
|
// member, at the configured volume; empty/invalid lists stay silent.
|
|
{
|
|
const names = ['solar_flare_01', 'solar_flare_02'];
|
|
const valid = new Set(names.map((n) => `sfx_${n}`));
|
|
let ok = true;
|
|
for (let i = 0; i < 200 && ok; i++) {
|
|
const scene = makeScene();
|
|
playSfxOneOfOn(scene, names);
|
|
ok = scene.calls.length === 1 && valid.has(scene.calls[0].key) && scene.calls[0].opts.volume === SFX.volume;
|
|
}
|
|
check('playSfxOneOfOn plays exactly one valid member of the set (200 draws)', ok);
|
|
|
|
const empty = makeScene();
|
|
playSfxOneOfOn(empty, []);
|
|
check('playSfxOneOfOn with an empty set → nothing plays', empty.calls.length === 0);
|
|
|
|
const junk = makeScene();
|
|
playSfxOneOfOn(junk, 'not-a-list');
|
|
check('playSfxOneOfOn with a non-list → nothing plays (no throw)', junk.calls.length === 0);
|
|
|
|
const noMgr = makeScene({ sound: false });
|
|
playSfxOneOfOn(noMgr, names);
|
|
check('playSfxOneOfOn with no sound manager → nothing plays (no throw)', noMgr.calls.length === 0);
|
|
}
|
|
|
|
console.log(failures === 0 ? '\nAll SFX voice tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
|
|
process.exit(failures === 0 ? 0 : 1);
|