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
+17
-7
@@ -46,7 +46,7 @@ import { dirname, join, relative, sep } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { Downloader } from './assets/downloader.mjs';
|
||||
import { loadIndexes } from './assets/cache.mjs';
|
||||
import { indexAudio } from './assets/audio.mjs';
|
||||
import { indexAudio, VOICE_DIRS } from './assets/audio.mjs';
|
||||
import { buildPlan } from './assets/plan.mjs';
|
||||
import { processModels, findLocalEnemyModels, localEnemySpineMeta, loadLocalEnemySpines, LOCAL_ENEMY_SPINES_FILE } from './assets/spine.mjs';
|
||||
import { collectLeaves, downloadLeaves, resolveTemplate, totalBytes, contentHash, droppedEntries, MANIFEST_VERSION } from './assets/manifest.mjs';
|
||||
@@ -66,7 +66,10 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
|
||||
--force re-download files even when present
|
||||
--offline no network: post-process what is on disk and rebuild data/assets.json
|
||||
--dry-run print the plan and exit
|
||||
--refresh-index re-download audio_data.json / models_data.json indexes
|
||||
--refresh-index re-download the audio_data.json / charword_table.json / models_data.json indexes
|
||||
--voice-lang=cn operator battle voice language: cn (default) | jp | en | kr
|
||||
--voice-all plan every official voice slot, including the prep-only lines no battle plays
|
||||
(干员报到 / 编入队伍 / 任命队长; 360 files / 19.3 MB more per run — off by default)
|
||||
--prune delete files under public/assets that the manifest no longer references
|
||||
(public/assets/local/** of tools/local-extract is never deleted); implies --allow-shrink
|
||||
--allow-shrink write data/assets.json even when it loses entries the current one has
|
||||
@@ -80,10 +83,10 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
|
||||
/**
|
||||
* Parse CLI flags.
|
||||
* @param {string[]} argv
|
||||
* @returns {{concurrency:number, force:boolean, offline:boolean, dryRun:boolean, refreshIndex:boolean, prune:boolean, allowShrink:boolean, addOnly:boolean, localSpines:boolean, help:boolean}}
|
||||
* @returns {{concurrency:number, force:boolean, offline:boolean, dryRun:boolean, refreshIndex:boolean, prune:boolean, allowShrink:boolean, addOnly:boolean, localSpines:boolean, voiceLang:string, voiceAll:boolean, help:boolean}}
|
||||
*/
|
||||
export function parseArgs(argv) {
|
||||
const o = { concurrency: 16, force: false, offline: false, dryRun: false, refreshIndex: false, prune: false, allowShrink: false, addOnly: false, localSpines: false, help: false };
|
||||
const o = { concurrency: 16, force: false, offline: false, dryRun: false, refreshIndex: false, prune: false, allowShrink: false, addOnly: false, localSpines: false, voiceLang: 'cn', voiceAll: false, help: false };
|
||||
for (const a of argv) {
|
||||
const [k, v] = a.split('=');
|
||||
if (k === '--concurrency') o.concurrency = Math.max(1, Math.min(64, parseInt(v, 10) || 16));
|
||||
@@ -95,6 +98,8 @@ export function parseArgs(argv) {
|
||||
else if (k === '--allow-shrink') o.allowShrink = true;
|
||||
else if (k === '--add-only') o.addOnly = true;
|
||||
else if (k === '--local-spines') o.localSpines = true;
|
||||
else if (k === '--voice-lang') { if (!VOICE_DIRS[v]) throw new Error(`unknown --voice-lang ${v} (cn | jp | en | kr)`); o.voiceLang = v; }
|
||||
else if (k === '--voice-all') o.voiceAll = true;
|
||||
else if (k === '--help' || k === '-h') o.help = true;
|
||||
else throw new Error(`unknown option ${a}\n${HELP}`);
|
||||
}
|
||||
@@ -198,6 +203,7 @@ function countStats(m, bytes, files) {
|
||||
modules: Object.keys(m.modules || {}).length,
|
||||
ui: Object.keys(m.ui || {}).length,
|
||||
sfxUnits: Object.keys(m.audio?.sfx?.units || {}).length,
|
||||
voiceChars: Object.keys(m.audio?.voice || {}).length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -258,7 +264,7 @@ async function main() {
|
||||
readJson('docs/research/05-enemies.json'),
|
||||
readJson('docs/research/05-maps.json'),
|
||||
]);
|
||||
const { audioData, modelsData } = await loadIndexes(ROOT, { refresh: opts.refreshIndex && !opts.offline, offline: opts.offline, log });
|
||||
const { audioData, modelsData, charword } = await loadIndexes(ROOT, { refresh: opts.refreshIndex && !opts.offline, offline: opts.offline, log });
|
||||
const audio = indexAudio(audioData);
|
||||
// The game data built by tools/build-data.mjs (when present) may reference more
|
||||
// spawnable enemies/tokens than research lists (e.g. 机变 enemy swaps): cover them too.
|
||||
@@ -269,7 +275,9 @@ async function main() {
|
||||
for (const b of Object.values(dataBosses || {})) if (b?.enemyKey && typeof b.handbookId === 'string') extraHandbook[b.enemyKey] = b.handbookId;
|
||||
const localEnemySpines = await syncLocalEnemySpines(opts);
|
||||
const plan = buildPlan({
|
||||
assets07, ops03, enemies05, maps05, audio, modelsData,
|
||||
assets07, ops03, enemies05, maps05, audio, modelsData, charword, voiceLang: opts.voiceLang,
|
||||
// default: only the slots a battle can play (plan.mjs VOICE_BATTLE_SLOTS); --voice-all takes the whole official set
|
||||
voiceSlots: opts.voiceAll ? null : undefined,
|
||||
extraEnemyIds: Object.keys(dataEnemies || {}),
|
||||
extraTokenIds: [...Object.keys(dataTokens || {}), ...extras.tokenIds],
|
||||
extraHandbook,
|
||||
@@ -281,7 +289,8 @@ async function main() {
|
||||
log(`[plan] ${leaves.length} files + ${plan.models.size} Spine models ` +
|
||||
`(${Object.keys(plan.template.chars).length} chars, ${Object.keys(plan.template.enemies).length} enemies, ` +
|
||||
`${Object.keys(plan.template.tokens).length} tokens, ${Object.keys(plan.template.ui).length} UI sprites, ` +
|
||||
`${Object.keys(plan.template.audio.sfx.units).length} units with SFX)`);
|
||||
`${Object.keys(plan.template.audio.sfx.units).length} units with SFX, ` +
|
||||
`${Object.keys(plan.template.audio.voice).length} operators with ${opts.voiceLang.toUpperCase()} voice)`);
|
||||
if (opts.dryRun) {
|
||||
for (const n of plan.notes) log(` note: ${n}`);
|
||||
return 0;
|
||||
@@ -370,6 +379,7 @@ async function main() {
|
||||
log(`on disk (manifest) : ${mb(s.bytes)} in ${s.files} files`);
|
||||
log(`chars ${s.chars} (Back model ${s.charsWithBack}) · enemies ${s.enemies} (Spine ${s.enemiesWithSpine}) · tokens ${s.tokens} (Spine ${s.tokensWithSpine}) · Spine models ${s.spineModels}`);
|
||||
log(`bonds ${s.bonds} · items ${s.items} · bands ${s.bands} · skill icons ${s.skills} · UI ${s.ui} · units with SFX ${s.sfxUnits}`);
|
||||
log(`operator battle voice: ${s.voiceChars} charIds (--voice-lang=${opts.voiceLang})`);
|
||||
log(`fonts: ${Object.values(fonts.files).map((f) => f.woff2 || f.original).join(', ') || 'none'}`);
|
||||
if (resolved.fallbacks.length) { log(`fallbacks used (${resolved.fallbacks.length}):`); for (const f of resolved.fallbacks.slice(0, 20)) log(` ${f}`); }
|
||||
if (downloadErrors.length) log(`download errors (${downloadErrors.length}, re-run to retry): ${downloadErrors.slice(0, 10).join(', ')}`);
|
||||
|
||||
Reference in new issue
Block a user