feat: improve portraits and combat audio

This commit is contained in:
2026-07-21 21:56:30 +09:00
parent 99a88c7128
commit f39de4f47d
27 changed files with 1396 additions and 154 deletions

View File

@@ -19,7 +19,9 @@ const server = await createServer({
});
try {
const { musicTracks, ambienceTracks, effectTracks } = await server.ssrLoadModule('/src/game/audio/audioAssets.ts');
const audioAssets = await server.ssrLoadModule('/src/game/audio/audioAssets.ts');
const { musicTracks, ambienceTracks, effectTracks } = audioAssets;
const effectPools = audioAssets.effectPools ?? {};
const scenarioModule = await server.ssrLoadModule('/src/game/data/scenario.ts');
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
const errors = [];
@@ -27,9 +29,11 @@ try {
validateTrackMap(errors, 'music', musicTracks, 'bgm');
validateTrackMap(errors, 'ambience', ambienceTracks, 'ambience');
validateTrackMap(errors, 'effect', effectTracks, 'sfx');
validateEffectPools(errors, effectPools, effectTracks);
validateEffectPoolRegistration(errors, effectPools);
validateAudioDirectoryCoverage(errors, musicTracks, ambienceTracks, effectTracks);
validateStoryBgmReferences(errors, scenarioModule, battleScenarios, musicTracks);
validateLiteralAudioCalls(errors, musicTracks, ambienceTracks, effectTracks);
validateLiteralAudioCalls(errors, musicTracks, ambienceTracks, effectTracks, effectPools);
if (errors.length) {
console.error(`Audio asset data verification failed with ${errors.length} issue(s):`);
@@ -41,7 +45,7 @@ try {
} else {
console.log(
`Verified ${Object.keys(musicTracks).length} music tracks, ${Object.keys(ambienceTracks).length} ambience tracks, ` +
`${Object.keys(effectTracks).length} effect tracks, ` +
`${Object.keys(effectTracks).length} effect tracks, ${Object.keys(effectPools).length} effect pools, ` +
`${collectStoryBgmReferences(scenarioModule, battleScenarios).length} story BGM references, and literal audio calls.`
);
}
@@ -49,6 +53,41 @@ try {
await server.close();
}
function validateEffectPools(errors, effectPools, effectTracks) {
const effectKeys = new Set(Object.keys(effectTracks));
Object.entries(effectPools).forEach(([poolKey, trackKeys]) => {
assertNonEmptyString(errors, poolKey, 'effect pool key');
if (!Array.isArray(trackKeys) || trackKeys.length === 0) {
errors.push(`effect pool "${poolKey}" must contain at least one track key`);
return;
}
const seenTrackKeys = new Set();
trackKeys.forEach((trackKey, index) => {
assertNonEmptyString(errors, trackKey, `effect pool "${poolKey}" track ${index}`);
if (!effectKeys.has(trackKey)) {
errors.push(`effect pool "${poolKey}" references unknown effect track "${trackKey}"`);
}
if (seenTrackKeys.has(trackKey)) {
errors.push(`effect pool "${poolKey}" repeats effect track "${trackKey}"`);
}
seenTrackKeys.add(trackKey);
});
});
}
function validateEffectPoolRegistration(errors, effectPools) {
if (Object.keys(effectPools).length === 0) {
return;
}
const mainSource = readFileSync(join('src', 'main.ts'), 'utf8');
if (!/\bregisterEffectPools\s*\(\s*effectPools\s*\)/.test(mainSource)) {
errors.push('src/main.ts must register exported effectPools with SoundDirector');
}
}
function validateTrackMap(errors, label, tracks, expectedFolder) {
Object.entries(tracks).forEach(([key, url]) => {
assertNonEmptyString(errors, key, `${label} track key`);
@@ -97,10 +136,10 @@ function validateStoryBgmReferences(errors, scenarioModule, battleScenarios, mus
});
}
function validateLiteralAudioCalls(errors, musicTracks, ambienceTracks, effectTracks) {
function validateLiteralAudioCalls(errors, musicTracks, ambienceTracks, effectTracks, effectPools) {
const musicKeys = new Set(Object.keys(musicTracks));
const ambienceKeys = new Set(Object.keys(ambienceTracks));
const effectKeys = new Set(Object.keys(effectTracks));
const effectKeys = new Set([...Object.keys(effectTracks), ...Object.keys(effectPools)]);
sourceFilesToScan.forEach((path) => {
const source = readFileSync(path, 'utf8');
@@ -116,7 +155,7 @@ function validateLiteralAudioCalls(errors, musicTracks, ambienceTracks, effectTr
});
collectLiteralCallKeys(source, 'playEffect').forEach(({ key, index }) => {
if (!effectKeys.has(key)) {
errors.push(`${path}:${lineForIndex(source, index)}: playEffect references unknown track "${key}"`);
errors.push(`${path}:${lineForIndex(source, index)}: playEffect references unknown track or pool "${key}"`);
}
});
});
@@ -165,13 +204,88 @@ function isStoryPage(page) {
function collectLiteralCallKeys(source, callName) {
const keys = [];
const pattern = new RegExp(`\\.${callName}\\(\\s*['"]([^'"]+)['"]`, 'g');
const pattern = new RegExp(`\\.${callName}\\s*\\(`, 'g');
for (const match of source.matchAll(pattern)) {
keys.push({ key: match[1], index: match.index ?? 0 });
const callIndex = match.index ?? 0;
const argumentStart = callIndex + match[0].length;
const argumentEnd = firstArgumentEnd(source, argumentStart);
const argumentSource = source.slice(argumentStart, argumentEnd);
const ternaryIndex = firstTernaryIndex(argumentSource);
const stringPattern = /(['"])([^'"\\]*(?:\\.[^'"\\]*)*)\1/g;
for (const stringMatch of argumentSource.matchAll(stringPattern)) {
if (ternaryIndex >= 0 && (stringMatch.index ?? 0) < ternaryIndex) {
continue;
}
if (stringMatch[2].includes('\\')) {
continue;
}
keys.push({ key: stringMatch[2], index: argumentStart + (stringMatch.index ?? 0) });
}
}
return keys;
}
function firstTernaryIndex(source) {
let quote = '';
for (let index = 0; index < source.length; index += 1) {
const character = source[index];
if (quote) {
if (character === '\\') {
index += 1;
} else if (character === quote) {
quote = '';
}
continue;
}
if (character === "'" || character === '"' || character === '`') {
quote = character;
continue;
}
if (character === '?' && source[index - 1] !== '?' && source[index + 1] !== '?' && source[index + 1] !== '.') {
return index;
}
}
return -1;
}
function firstArgumentEnd(source, startIndex) {
let nestedDepth = 0;
let quote = '';
for (let index = startIndex; index < source.length; index += 1) {
const character = source[index];
if (quote) {
if (character === '\\') {
index += 1;
} else if (character === quote) {
quote = '';
}
continue;
}
if (character === "'" || character === '"' || character === '`') {
quote = character;
continue;
}
if (character === '(' || character === '[' || character === '{') {
nestedDepth += 1;
continue;
}
if (character === ')' || character === ']' || character === '}') {
if (nestedDepth === 0) {
return index;
}
nestedDepth -= 1;
continue;
}
if (character === ',' && nestedDepth === 0) {
return index;
}
}
return source.length;
}
function assetUrlToPath(url) {
const marker = 'src/assets/audio/';
const index = url.indexOf(marker);