feat: improve portraits and combat audio
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user