Operator battle voices: the official combat lines play by voice slot during battles, one at a time (#73)
Deploy, skill and result lines come from the operators' official voice slots; a newer line takes the channel over (stale callbacks of the replaced line no longer release it), and the result line is spoken by an operator of the player's own battle. Contributed by @Convey123.
This commit is contained in:
1 parent
8774379d3b
commit
54cdad2602
22 files changed
+823
-49
No files matched your search
@@ -19,8 +19,9 @@
|
||||
// feedback after 0.1.0 (DESIGN §21, v0.1.1) including batch 6 (§21.21–§21.25: 坚固维式重锤 once per deployment, 起飞,
|
||||
// fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7) and
|
||||
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2),
|
||||
// the closing additions §21.26–§21.28 (GitHub issues #1 / #5 / #8) and the owner's deliberate trigger deviation for six
|
||||
// 重装 skills (§21.29, GitHub issue #4 / PR #12).
|
||||
// the closing additions §21.26–§21.28 (GitHub issues #1 / #5 / #8), the owner's deliberate trigger deviation for six
|
||||
// 重装 skills (§21.29, GitHub issue #4 / PR #12) and the operator battle voice the user asked for the same day (§21.30,
|
||||
// battle only — the 休整期 is silent).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -845,3 +846,57 @@ test('the deliberate trigger deviation (DESIGN §21.29): six 重装 skills DEFAU
|
||||
assert.match(PLAYING, /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放/);
|
||||
assert.match(doc('CHANGELOG.md'), /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能改为攻击范围内有敌人时就释放/);
|
||||
});
|
||||
|
||||
test('干员战斗语音 (DESIGN §21.30): the manifest data, the official priorities, and the 休整期 stays silent', async () => {
|
||||
const { VOICE_PRIORITY, VOICE_COOLDOWN_MS, resultVoiceSlot } = await import('../public/js/audio.js');
|
||||
const manifest = JSON.parse(readFileSync(join(ROOT, 'data/assets.json'), 'utf8'));
|
||||
const voice = manifest.audio?.voice ?? {};
|
||||
const charIds = Object.keys(voice);
|
||||
assert.ok(charIds.length >= 100, `${charIds.length} operators carry official battle voice`);
|
||||
assert.equal(manifest.stats.voiceChars, charIds.length, 'stats.voiceChars counts them');
|
||||
// one operator carries every slot a battle can play — and none of the prep-only ones (plan.mjs VOICE_BATTLE_SLOTS):
|
||||
// 干员报到 / 编入队伍 / 任命队长 are never requested by the client, so planning them only made every `npm run assets`
|
||||
// download 360 files (19.3 MB, one per operator and slot) nobody hears. `--voice-all` brings the complete set back.
|
||||
const slots = ['start', 'faceEnemy', 'select', 'place', 'skill1', 'skill2', 'skill3', 'skill4', 'resultFour', 'resultThree', 'resultTwo', 'resultLose'];
|
||||
const prepOnly = ['gacha', 'squad', 'squadFirst'];
|
||||
const lines = [];
|
||||
const walk = (x) => {
|
||||
if (typeof x === 'string') lines.push(x);
|
||||
else if (Array.isArray(x)) x.forEach(walk);
|
||||
else if (x && typeof x === 'object') Object.values(x).forEach(walk);
|
||||
};
|
||||
for (const id of charIds) {
|
||||
for (const s of slots) assert.ok(voice[id][s], `${id}.${s}`);
|
||||
for (const s of prepOnly) assert.equal(voice[id][s], undefined, `${id}.${s} is not planned by default`);
|
||||
walk(voice[id]);
|
||||
}
|
||||
// a battle slot usually carries several lines (选中干员 / 部署 have two), so the battle set alone stays well above 10 each
|
||||
assert.ok(lines.length >= charIds.length * 10, `${lines.length} voice lines for ${charIds.length} operators`);
|
||||
for (const u of lines) assert.match(u, /^\/assets\/audio\/voice\/cn\/char_[^/]+\/cn_\d+\.mp3$/);
|
||||
// the official scheduling numbers (audio_data.json battleVoice.voiceTypeOptions)
|
||||
assert.equal(VOICE_PRIORITY.start, 100);
|
||||
assert.equal(VOICE_PRIORITY.faceEnemy, 90);
|
||||
assert.equal(VOICE_PRIORITY.skill1, 70);
|
||||
assert.equal(VOICE_PRIORITY.place, 20);
|
||||
assert.equal(VOICE_PRIORITY.select, 10);
|
||||
assert.equal(VOICE_COOLDOWN_MS.skill1, 10000);
|
||||
assert.equal(VOICE_COOLDOWN_MS.faceEnemy, 3000);
|
||||
assert.equal(VOICE_COOLDOWN_MS.select, 1500);
|
||||
assert.equal(resultVoiceSlot({ perfect: true }), 'resultThree');
|
||||
// the docs
|
||||
assert.match(DESIGN, /### 21\.30 /);
|
||||
assert.match(DESIGN, /\*\*Where each line plays — battle only\.\*\*/);
|
||||
assert.match(doc('docs/ASSETS.md'), /\| 干员战斗语音 \|/);
|
||||
assert.match(doc('docs/ASSETS.md'), /voiceChars/);
|
||||
assert.match(SIM, /\['engage', id\]/);
|
||||
// the code: every slot the client asks for comes from a running battle's own stream — the three prep-only lines
|
||||
// (干员报到 / 编入队伍 / 任命队长) are never requested, and 选中干员 sits behind the panel's combat flag
|
||||
const panel = doc('public/js/ui/detailPanel.js');
|
||||
const game = doc('public/js/screens/game.js');
|
||||
for (const [name, src] of [['audio.js', doc('public/js/audio.js')], ['game.js', game], ['detailPanel.js', panel]]) {
|
||||
assert.ok(!/voice\([^)]*'(gacha|squad|squadFirst)'/.test(src), `${name}: no prep slot is played`);
|
||||
}
|
||||
assert.match(panel, /const selectKey = voice && detail\?\.type === 'chess'/);
|
||||
assert.match(game, /voice=\$\{combat\}/);
|
||||
assert.match(game, /audio\.voice\(charId, resultVoiceSlot\(/);
|
||||
});
|
||||
Reference in new issue
Block a user