87 lines
5.9 KiB
JavaScript
87 lines
5.9 KiB
JavaScript
// test/ui/feedback3-audio-mix.test.js — community report #30 (0.1.3): "狗、飞机敌人音效声音过大". The official attack bank of
|
||
// 猎狗pro / 深池侦察犬 is 80 % silence (an empty asset weighs 80, e_atk_gopro_* 20), but the asset tool kept only the file
|
||
// paths, so the client barked on every attack at a fixed 0.55; 妖怪's attack bank is official at volume 0.7. The bank's
|
||
// play chance and volume now travel as sfx.units[id].mix (tools/assets/audio.mjs bankMix → plan.mjs) and audio.js plays a
|
||
// unit's own sound with chance `p` at its base gain × min(1, vol) — never louder than before.
|
||
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { AudioManager, unitGain, unitSoundPlays } from '../../public/js/audio.js';
|
||
import { indexAudio, pickUnitSfx, bankMix } from '../../tools/assets/audio.mjs';
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const manifest = JSON.parse(readFileSync(path.join(ROOT, 'data', 'assets.json'), 'utf8'));
|
||
const UNITS = manifest.audio.sfx.units;
|
||
const OFFICIAL = path.join(ROOT, '.cache', 'gamedata', 'excel', 'audio_data.json');
|
||
|
||
/** An audio manager with a context, recording what it would play: [{ url, volume, unitKey }]. */
|
||
function manager(rolls) {
|
||
const a = new AudioManager({ win: null, getManifest: () => manifest, random: () => rolls.shift() ?? 0 });
|
||
a.ctx = {};
|
||
a.played = [];
|
||
a._play = (url, o) => { a.played.push({ url, volume: o.volume, unitKey: o.unitKey }); };
|
||
return a;
|
||
}
|
||
|
||
test('the bank mix: an empty asset is a chance of silence, the played file\'s volume (bankMix, indexAudio().mixOf)', () => {
|
||
const gopro = [
|
||
{ asset: '', weight: 80, minVolume: 1, maxVolume: 1 },
|
||
{ asset: 'Audio/Sound_Beta_2/Enemy/e_atk/e_atk_gopro_1', weight: 10, minVolume: 1, maxVolume: 1 },
|
||
{ asset: 'Audio/Sound_Beta_2/Enemy/e_atk/e_atk_gopro_2', weight: 10, minVolume: 1, maxVolume: 1 },
|
||
];
|
||
assert.deepEqual(bankMix(gopro, 'enemy/e_atk/e_atk_gopro_1.mp3'), { p: 0.2 });
|
||
assert.deepEqual(bankMix([{ asset: 'Audio/Sound_Beta_2/Enemy/e_atk/e_atk_vehicle_n', weight: 1, minVolume: 0.7, maxVolume: 0.7 }], 'enemy/e_atk/e_atk_vehicle_n.mp3'), { vol: 0.7 });
|
||
assert.equal(bankMix([{ asset: 'Audio/Sound_Beta_2/P/x', weight: 1, minVolume: 1, maxVolume: 1 }], 'p/x.mp3'), null, 'the default: nothing stored');
|
||
const idx = indexAudio({ soundFXBanks: [
|
||
{ name: 'battle.ON_ABILITY_ON.enemy_1000_gopro_2.combat', sounds: gopro },
|
||
{ name: 'battle.ON_ABILITY_HIT.enemy_1000_gopro_2.combat', sounds: [{ asset: 'Audio/Sound_Beta_2/Enemy/e_imp/e_imp_general_w', weight: 1, minVolume: 1, maxVolume: 1 }] },
|
||
] });
|
||
const sfx = pickUnitSfx(idx.unitBanks.get('enemy_1000_gopro_2'));
|
||
assert.deepEqual(sfx.attack, ['enemy/e_atk/e_atk_gopro_1.mp3', 'enemy/e_atk/e_atk_gopro_2.mp3']);
|
||
assert.deepEqual(idx.mixOf(sfx.attack), { p: 0.2 }, 'the picked list carries its bank\'s mix');
|
||
assert.equal(idx.mixOf(sfx.hit), null);
|
||
assert.equal(idx.mixOf(['enemy/e_atk/e_atk_gopro_1.mp3']), null, 'only the bank\'s own list');
|
||
});
|
||
|
||
test('data/assets.json carries the official mix: dogs 20 %, 妖怪 / 威龙 0.7 (and matches audio_data.json when cached)', () => {
|
||
for (const id of ['enemy_1000_gopro_2', 'enemy_1165_duhond', 'enemy_1165_duhond_2']) assert.deepEqual(UNITS[id].mix, { attack: { p: 0.2 } }, id);
|
||
for (const id of ['enemy_1005_yokai_2', 'enemy_1005_yokai_3']) assert.deepEqual(UNITS[id].mix, { attack: { vol: 0.7 } }, id);
|
||
assert.equal(UNITS.char_002_amiya?.mix, undefined, 'default banks store nothing');
|
||
if (!existsSync(OFFICIAL)) return;
|
||
const banks = new Map(JSON.parse(readFileSync(OFFICIAL, 'utf8')).soundFXBanks.map((b) => [b.name, b]));
|
||
const dog = banks.get('battle.ON_ABILITY_ON.enemy_1165_duhond.combat').sounds;
|
||
assert.equal(dog.filter((s) => !s.asset).reduce((n, s) => n + s.weight, 0) / dog.reduce((n, s) => n + s.weight, 0), 0.8, 'official: 80 % silence');
|
||
assert.equal(banks.get('battle.ON_ABILITY_ON.enemy_1005_yokai_2.attack').sounds[0].maxVolume, 0.7);
|
||
});
|
||
|
||
test('a dog barks only on its chance and never falls back to the generic enemy sound; at most the old gain', () => {
|
||
assert.equal(unitSoundPlays({ p: 0.2 }, 0.19), true);
|
||
assert.equal(unitSoundPlays({ p: 0.2 }, 0.2), false);
|
||
assert.equal(unitSoundPlays(null, 0.99), true);
|
||
const a = manager([0.5, 0.9, 0.1, 0.3, 0.05]);
|
||
a.setFieldUnits([{ id: 1, side: 'enemy', kind: 'enemy', spine: 'enemy_1000_gopro_2' }, { id: 2, side: 'ally', kind: 'op', spine: 'char_002_amiya' }]);
|
||
for (let i = 0; i < 5; i++) { a.limiter = new a.limiter.constructor(); a.handleBattleEvents([['atk', 1, 2, 'none']]); }
|
||
const barks = a.played.filter((p) => p.url === UNITS.enemy_1000_gopro_2.attack);
|
||
assert.equal(barks.length, 2, 'rolls 0.1 and 0.05 of 0.5 / 0.9 / 0.1 / 0.3 / 0.05');
|
||
for (const b of barks) assert.equal(b.volume, 0.55, 'official volume 1: the same gain as before');
|
||
assert.equal(a.played.length, 2, 'the silent rolls play nothing — not the generic enemyHit either');
|
||
});
|
||
|
||
test('妖怪 plays at 0.55 × 0.7; a bank louder than 1 stays at the old gain; die / born use the mix too', () => {
|
||
assert.equal(unitGain(0.55, { vol: 0.7 }), 0.55 * 0.7);
|
||
assert.equal(unitGain(0.55, { vol: 2 }), 0.55, 'never louder than before');
|
||
assert.equal(unitGain(0.8, null), 0.8);
|
||
const a = manager([]);
|
||
assert.equal(a.unit('enemy_1005_yokai_2', 'attack', 7), true);
|
||
assert.ok(Math.abs(a.played[0].volume - 0.385) < 1e-9);
|
||
assert.equal(a.unit('char_1028_texas2', 'hit', 8), true);
|
||
assert.equal(a.played[1].volume, 0.55, 'official 2: unchanged');
|
||
a.setFieldUnits([{ id: 3, side: 'ally', kind: 'op', spine: 'char_2026_yu', defId: 'chess_char_x' }]);
|
||
a.handleBattleEvents([['deploy', 3]]);
|
||
const born = a.played.find((p) => p.url === UNITS.char_2026_yu.born);
|
||
assert.ok(born && Math.abs(born.volume - 0.8 * 0.75) < 1e-9, '余 ON_UNIT_BORN at 0.75');
|
||
});
|