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

@@ -25,7 +25,13 @@ try {
storyBackgroundKeysForPages,
storyBackgroundPageHasFixedAssignment
} = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
const { portraitAssetEntriesForKey, portraitKeyForSpeaker } = await server.ssrLoadModule('/src/game/data/portraitAssets.ts');
const {
portraitAssets,
portraitAssetEntriesForKey,
portraitAssetEntriesForStoryContext,
portraitTextureKeysForKey,
portraitKeyForSpeaker
} = await server.ssrLoadModule('/src/game/data/portraitAssets.ts');
const { storyCutsceneActorProfileFor } = await server.ssrLoadModule('/src/game/data/storyCutscenes.ts');
const { hasUnitSheetAsset } = await server.ssrLoadModule('/src/game/data/unitAssets.ts');
@@ -46,6 +52,14 @@ try {
let pageCount = 0;
let cutsceneCount = 0;
validatePortraitVersionSelection({
errors,
portraitAssets,
portraitAssetEntriesForKey,
portraitAssetEntriesForStoryContext,
portraitTextureKeysForKey
});
storySources.forEach(({ source, pages }) => {
if (!Array.isArray(pages) || pages.length === 0) {
errors.push(`${source}: story page list must not be empty`);
@@ -86,6 +100,7 @@ try {
storyBackgroundAssets,
storyBackgroundKeysFor,
portraitAssetEntriesForKey,
portraitAssetEntriesForStoryContext,
portraitKeyForSpeaker,
storyCutsceneActorProfileFor,
hasUnitSheetAsset
@@ -124,6 +139,152 @@ try {
await server.close();
}
function validatePortraitVersionSelection(context) {
const {
errors,
portraitAssets,
portraitAssetEntriesForKey,
portraitAssetEntriesForStoryContext,
portraitTextureKeysForKey
} = context;
const zhugeLiangTextureKeys = portraitTextureKeysForKey('zhugeLiang');
const requiredZhugeLiangTextureKeys = [
'portrait-zhuge-liang',
'portrait-zhuge-liang-campaign-v2',
'portrait-zhuge-liang-northern-v2',
'portrait-zhuge-liang-redcliffs-v2'
];
const supersededZhugeLiangTextureKeys = [
'portrait-zhuge-liang-campaign',
'portrait-zhuge-liang-northern',
'portrait-zhuge-liang-redcliffs'
];
requiredZhugeLiangTextureKeys.forEach((textureKey) => {
if (!zhugeLiangTextureKeys.includes(textureKey)) {
errors.push(`portrait version selection: missing current Zhuge Liang portrait "${textureKey}"`);
}
});
supersededZhugeLiangTextureKeys.forEach((textureKey) => {
if (zhugeLiangTextureKeys.includes(textureKey)) {
errors.push(`portrait version selection: superseded Zhuge Liang portrait remains selectable "${textureKey}"`);
}
});
const zhugeLiangStoryCases = [
{
context: { id: 'seventeenth-zhuge-liang-counsel', background: 'story-wolong' },
expected: 'portrait-zhuge-liang'
},
{
context: { id: 'eighteenth-red-cliffs-wind', background: 'story-wolong' },
expected: 'portrait-zhuge-liang-redcliffs-v2'
},
{
context: { id: 'twenty-first-zhuge-wind', background: 'story-red-cliffs' },
expected: 'portrait-zhuge-liang-redcliffs-v2'
},
{
context: { id: 'fiftieth-third-capture-plan', background: 'story-nanzhong-captures' },
expected: 'portrait-zhuge-liang-campaign-v2'
},
{
context: { id: 'thirty-fifth-hanzhong-front', background: 'story-northern' },
expected: 'portrait-zhuge-liang-campaign-v2'
},
{
context: { id: 'northern-prep-back-secured', background: 'story-northern' },
expected: 'portrait-zhuge-liang-northern-v2'
},
{
context: { id: 'fifty-fifth-qishan-road-opens', background: 'story-northern' },
expected: 'portrait-zhuge-liang-northern-v2'
},
{
context: { id: 'sixtieth-wudu-yinping-order', background: 'story-wudu-yinping' },
expected: 'portrait-zhuge-liang-northern-v2'
}
];
zhugeLiangStoryCases.forEach(({ context: storyContext, expected }) => {
const selected = portraitAssetEntriesForStoryContext('zhugeLiang', storyContext).map(
({ textureKey }) => textureKey
);
if (selected.length !== 1 || selected[0] !== expected) {
errors.push(
`portrait story context: ${storyContext.id} should select "${expected}", received [${selected.join(', ')}]`
);
}
});
const zhugeLiangBaseRevisionFixture = 'zhuge-liang-v2';
const previousBaseRevision = portraitAssets[zhugeLiangBaseRevisionFixture];
portraitAssets[zhugeLiangBaseRevisionFixture] = `virtual:${zhugeLiangBaseRevisionFixture}`;
try {
const selected = portraitAssetEntriesForStoryContext('zhugeLiang', {
id: 'seventeenth-zhuge-liang-counsel',
background: 'story-wolong'
}).map(({ textureKey }) => textureKey);
if (selected.length !== 1 || selected[0] !== 'portrait-zhuge-liang-v2') {
errors.push(
`portrait story context: a revised base portrait should remain in the base route, received [${selected.join(', ')}]`
);
}
} finally {
if (previousBaseRevision === undefined) {
delete portraitAssets[zhugeLiangBaseRevisionFixture];
} else {
portraitAssets[zhugeLiangBaseRevisionFixture] = previousBaseRevision;
}
}
const fixtureSlug = 'portrait-version-fixture';
const fixtureSlugs = [
fixtureSlug,
`${fixtureSlug}-campaign`,
`${fixtureSlug}-campaign-v2`,
`${fixtureSlug}-campaign-v10`,
`${fixtureSlug}-northern`,
`${fixtureSlug}-redcliffs-v2`,
`${fixtureSlug}-redcliffs-v3`,
`${fixtureSlug}-vanguard`,
`${fixtureSlug}-veteran`
];
const expectedFixtureTextureKeys = [
`portrait-${fixtureSlug}`,
`portrait-${fixtureSlug}-campaign-v10`,
`portrait-${fixtureSlug}-northern`,
`portrait-${fixtureSlug}-redcliffs-v3`,
`portrait-${fixtureSlug}-vanguard`,
`portrait-${fixtureSlug}-veteran`
];
fixtureSlugs.forEach((slug) => {
portraitAssets[slug] = `virtual:${slug}`;
});
try {
const entryTextureKeys = portraitAssetEntriesForKey(fixtureSlug).map((entry) => entry.textureKey);
const textureKeys = portraitTextureKeysForKey(fixtureSlug);
if (entryTextureKeys.join('\n') !== expectedFixtureTextureKeys.join('\n')) {
errors.push(
`portrait version selection: entries should keep independent variants and only the highest numeric -vN suffix; ` +
`received [${entryTextureKeys.join(', ')}]`
);
}
if (textureKeys.join('\n') !== expectedFixtureTextureKeys.join('\n')) {
errors.push(
`portrait version selection: texture keys should keep independent variants and only the highest numeric -vN suffix; ` +
`received [${textureKeys.join(', ')}]`
);
}
} finally {
fixtureSlugs.forEach((slug) => {
delete portraitAssets[slug];
});
}
}
function collectStorySources(scenarioModule, battleScenarios) {
const sources = [];
const seenArrays = new WeakSet();
@@ -350,13 +511,13 @@ function validateRepeatedStoryBackgroundDistribution(errors, selectedBackgroundU
}
function validatePortrait(context, pageContext) {
const { errors, page, usedPortraitKeys, portraitAssetEntriesForKey, portraitKeyForSpeaker } = context;
const { errors, page, usedPortraitKeys, portraitAssetEntriesForStoryContext, portraitKeyForSpeaker } = context;
const portraitKey = page.portrait ?? portraitKeyForSpeaker(page.speaker);
if (!portraitKey) {
return;
}
const entries = portraitAssetEntriesForKey(portraitKey);
const entries = portraitAssetEntriesForStoryContext(portraitKey, page);
if (entries.length === 0) {
errors.push(`${pageContext}: missing portrait asset "${portraitKey}"`);
}