Files
ark-stronghold-protocol/test/ui/feedback3-audio-mix.test.js
T

87 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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');
});