feedback5: merge feedback4 (beb0296) — PR #73 (operator battle voices) ported to the split layout (the voice setting into ui/gameLogic/settings.js, the gameLogic.js shim unchanged; onResult and DetailPanel voice stay in screens/game.js, which needed its store import back; the sim's 'engage' in server/sim/ai.js performAttack, still the one battle/combat.js and devices.js call; plan.mjs / fetch-assets.mjs merged with --add-only, extraOperators and moduleTypes; the 干员语音 slider through t() with English); golden: roster / bonds / fields / standins / diy differ only in events.engage, matches unchanged
Ported by hand: - public/js/ui/gameLogic.js is a re-export shim on feedback5: #73's DEFAULT_SETTINGS / sanitizeSettings `voice` hunk went into public/js/ui/gameLogic/settings.js. - public/js/screens/game.js: MatchScreen still lives there, so #73's onResult (battleRunner 'result' → resultSpeaker / resultVoiceSlot) and `voice=${combat}` on the DetailPanel auto-merged in place. feedback5's game.js no longer imported `store` (the split moved its other users), so onResult's store.get() would have thrown into its try/catch and the 结算 line would never have played: `store` is imported again. `localAsset` stays dropped. - public/js/ui/settings.js (i18n phase 1): the slider is `label=${t('干员语音')}`, public/i18n/en.json "干员语音": "Operator Voices" (next to Music / Sound Effects). - tools/assets/plan.mjs: buildPlan takes #73's charword / voiceLang / voiceSlots and feedback5's extraOperators / moduleTypes; the template has `modules` and audio { bgm, bossBgm, voice, sfx }. `chars` now includes the 自选 picks, so the voice loop's comment says so (the next `npm run assets` plans their lines too). - tools/fetch-assets.mjs: parseArgs keeps --add-only beside --voice-lang / --voice-all. - data/assets.json: feedback5's manifest plus #73's audio.voice (120 operators, after bossBgm, the plan order); stats files +1680 and bytes +42 041 118 as measured on feedback4, voiceChars 120; the hash is recomputed with the generator's contentHash. No other entry changed. - docs/ASSETS.md: the stats line lists modules and voiceChars. DESIGN §21.30 stays where feedback4 put it; SIM.md's 'engage' line and docs-consistency's §21.30 test auto-merged (game.js / detailPanel.js still hold the pinned code). - Auto-merged and checked: audio.js, battle/runner.js emit('result'), components.js mic icon, detailPanel.js voice prop, shared/protocol.js EV.ENGAGE, server/sim/ai.js, tools/assets/audio.mjs / cache.mjs, #73's tests. Where #73 met feedback5 work: - 补位: a chess fielded as its stand-in shows the stand-in in the detail card, and audio.js keys battle lines by UnitInfo spine (the stand-in's model). The 选中干员 line now takes detail.standIn.charId first, so the stand-in speaks, not the operator it replaces (test/ui/standin-ui.test.js pins it). A 自选 record is already the pick's. - The sim's 'engage': stand-in and 自选 units attack through the same performAttack, so it fires for them as well. Golden regenerated: every moved scenario differs only in events.engage (the new event's count; no gameplay change), proven by a field-path diff against feedback5: - roster: all 49; bonds: all 46; fields: all 22; standins: all 10 (the stand-ins engage). - diy: 101 of 109. Unmoved: diy-010/011/012/092/093 (闪灵, 夜莺, 凯尔希 squads: they only heal, and a heal never engages) and diy-077 (薇薇安娜), diy-080/081 (赫德雷), whose picks make no attack in those battles (already so on feedback5). - matches: unchanged.
This commit is contained in:
commit
c5e8d13636
29 files changed
+1059
-278
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, readdirSync } from 'node:fs';
|
||||
@@ -857,3 +858,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