feat: deepen battle atmosphere and portrait eras
This commit is contained in:
@@ -9,6 +9,7 @@ const validDirections = new Set(['south', 'east', 'north', 'west']);
|
||||
const portraitlessActorIds = new Set(['rebel-leader']);
|
||||
const maxAdjacentBackgroundDuplicateRate = 0.02;
|
||||
const maxAdjacentBackgroundRun = 3;
|
||||
const expectedStoryPageCount = 466;
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
@@ -27,6 +28,7 @@ try {
|
||||
} = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
|
||||
const {
|
||||
portraitAssets,
|
||||
portraitAssetEntryForStoryContext,
|
||||
portraitAssetEntriesForKey,
|
||||
portraitAssetEntriesForStoryContext,
|
||||
portraitTextureKeysForKey,
|
||||
@@ -49,12 +51,18 @@ try {
|
||||
avoidableDuplicatePairs: 0,
|
||||
maxRun: 0
|
||||
};
|
||||
const portraitSelectionMetrics = {
|
||||
pages: 0,
|
||||
dialoguePortraits: 0,
|
||||
cutsceneActorPortraits: 0
|
||||
};
|
||||
let pageCount = 0;
|
||||
let cutsceneCount = 0;
|
||||
|
||||
validatePortraitVersionSelection({
|
||||
errors,
|
||||
portraitAssets,
|
||||
portraitAssetEntryForStoryContext,
|
||||
portraitAssetEntriesForKey,
|
||||
portraitAssetEntriesForStoryContext,
|
||||
portraitTextureKeysForKey
|
||||
@@ -99,11 +107,13 @@ try {
|
||||
usedCutsceneActorIds,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
portraitAssetEntryForStoryContext,
|
||||
portraitAssetEntriesForKey,
|
||||
portraitAssetEntriesForStoryContext,
|
||||
portraitKeyForSpeaker,
|
||||
storyCutsceneActorProfileFor,
|
||||
hasUnitSheetAsset
|
||||
hasUnitSheetAsset,
|
||||
portraitSelectionMetrics
|
||||
});
|
||||
if (page.cutscene) {
|
||||
cutsceneCount += 1;
|
||||
@@ -114,6 +124,14 @@ try {
|
||||
validateStoryBackgroundVariantCoverage(errors, selectedBackgroundKeys);
|
||||
validateRepeatedStoryBackgroundDistribution(errors, selectedBackgroundUrlsByBase);
|
||||
validateStoryBackgroundAdjacencyMetrics(errors, adjacencyMetrics);
|
||||
if (pageCount !== expectedStoryPageCount) {
|
||||
errors.push(`story page selection coverage: expected ${expectedStoryPageCount} pages, received ${pageCount}`);
|
||||
}
|
||||
if (portraitSelectionMetrics.pages !== pageCount) {
|
||||
errors.push(
|
||||
`story portrait selection coverage: checked ${portraitSelectionMetrics.pages}/${pageCount} pages`
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`Story asset data verification failed with ${errors.length} issue(s):`);
|
||||
@@ -130,7 +148,9 @@ try {
|
||||
`Verified ${pageCount} story page references (${uniquePageIds.size} unique pages), ` +
|
||||
`${cutsceneCount} cutscenes, ${usedBackgroundKeys.size} background candidates, ` +
|
||||
`${selectedBackgroundKeys.size} selected backgrounds, ` +
|
||||
`${usedPortraitKeys.size} portrait keys, and ${usedCutsceneActorIds.size} cutscene actor profiles; ` +
|
||||
`${usedPortraitKeys.size} portrait keys, ${portraitSelectionMetrics.dialoguePortraits} dialogue portrait selections, ` +
|
||||
`${portraitSelectionMetrics.cutsceneActorPortraits} cutscene actor portrait selections, and ` +
|
||||
`${usedCutsceneActorIds.size} cutscene actor profiles; ` +
|
||||
`${adjacencyMetrics.duplicatePairs}/${adjacencyMetrics.pairs} adjacent background repeats ` +
|
||||
`(${(adjacentDuplicateRate * 100).toFixed(1)}%), max run ${adjacencyMetrics.maxRun}.`
|
||||
);
|
||||
@@ -143,6 +163,7 @@ function validatePortraitVersionSelection(context) {
|
||||
const {
|
||||
errors,
|
||||
portraitAssets,
|
||||
portraitAssetEntryForStoryContext,
|
||||
portraitAssetEntriesForKey,
|
||||
portraitAssetEntriesForStoryContext,
|
||||
portraitTextureKeysForKey
|
||||
@@ -217,6 +238,61 @@ function validatePortraitVersionSelection(context) {
|
||||
}
|
||||
});
|
||||
|
||||
const contextualPortraitRouteCases = [
|
||||
{ key: 'liuBei', id: 'oath-liu-bei', background: 'title-taoyuan', expected: 'portrait-liu-bei-yellow-turban' },
|
||||
{ key: 'liuBei', id: 'seventh-liu-bei-xuzhou', background: 'story-xuzhou', expected: 'portrait-liu-bei' },
|
||||
{ key: 'liuBei', id: 'seventeenth-liu-bei-third-visit', background: 'story-wolong', expected: 'portrait-liu-bei-campaign' },
|
||||
{ key: 'liuBei', id: 'baidi-entrustment-liu-bei', background: 'story-baidi-entrustment', expected: 'portrait-liu-bei-shuhan' },
|
||||
{ key: 'guanYu', id: 'oath-brothers', background: 'title-taoyuan', expected: 'portrait-guan-yu-yellow-turban' },
|
||||
{ key: 'guanYu', id: 'seventh-guan-yu-field', background: 'story-xuzhou', expected: 'portrait-guan-yu' },
|
||||
{ key: 'guanYu', id: 'eighteenth-generals-doubt', background: 'story-wolong', expected: 'portrait-guan-yu-campaign' },
|
||||
{ key: 'guanYu', id: 'forty-fourth-guan-yu-resolve', background: 'story-maicheng-isolation', expected: 'portrait-guan-yu-jingzhou' },
|
||||
{ key: 'zhangFei', id: 'zhang-fei-joins', background: 'story-three-heroes', expected: 'portrait-zhang-fei-yellow-turban' },
|
||||
{ key: 'zhangFei', id: 'seventh-zhang-fei-charge', background: 'story-xuzhou', expected: 'portrait-zhang-fei' },
|
||||
{ key: 'zhangFei', id: 'nineteenth-zhang-fei-bridge', background: 'story-red-cliffs', expected: 'portrait-zhang-fei-campaign' },
|
||||
{ key: 'zhangFei', id: 'thirty-fifth-hanzhong-front', background: 'story-northern', expected: 'portrait-zhang-fei-ba-command' },
|
||||
{ key: 'zhaoYun', id: 'sixteenth-zhao-yun-returns', background: 'story-wolong', expected: 'portrait-zhao-yun' },
|
||||
{ key: 'zhaoYun', id: 'nineteenth-zhao-yun-rescue', background: 'story-red-cliffs', expected: 'portrait-zhao-yun-changban' },
|
||||
{ key: 'zhaoYun', id: 'twenty-third-jingzhou-cause', background: 'story-yizhou', expected: 'portrait-zhao-yun-campaign' },
|
||||
{ key: 'zhaoYun', id: 'northern-prep-roster-council', background: 'story-northern', expected: 'portrait-zhao-yun-veteran' },
|
||||
{ key: 'jiangWei', id: 'fifty-seventh-victory-jiang-wei-joins', background: 'story-jieting-crisis', expected: 'portrait-jiang-wei-tianshui' },
|
||||
{ key: 'jiangWei', id: 'fifty-eighth-jiang-wei-first-sortie', background: 'story-jieting-crisis', expected: 'portrait-jiang-wei-tianshui' },
|
||||
{ key: 'jiangWei', id: 'sixty-sixth-inherited-ledger', background: 'story-northern', expected: 'portrait-jiang-wei-campaign' },
|
||||
{ key: 'cao-cao', id: 'fifth-liu-bei-choice', background: 'story-anti-dong', expected: 'portrait-cao-cao' },
|
||||
{ key: 'cao-cao', id: 'eleventh-arrive-cao-cao-camp', background: 'story-wandering', expected: 'portrait-cao-cao-campaign' },
|
||||
{ key: 'cao-cao', id: 'twenty-second-cao-cao-retreat', background: 'story-red-cliffs', expected: 'portrait-cao-cao-redcliffs' },
|
||||
{ key: 'simaYi', id: 'fourteenth-secret-edict-shadow', background: 'story-wandering', expected: 'portrait-sima-yi' },
|
||||
{ key: 'simaYi', id: 'fifty-fourth-victory-meng-huo-yields', background: 'story-nanzhong-captures', expected: 'portrait-sima-yi-campaign' },
|
||||
{ key: 'simaYi', id: 'sixty-second-sima-yi-delay-line', background: 'story-qishan-renewed', expected: 'portrait-sima-yi-qishan' },
|
||||
{ key: 'meng-huo', id: 'forty-seventh-meng-huo-shadow', background: 'story-nanzhong', expected: 'portrait-meng-huo' },
|
||||
{ key: 'meng-huo', id: 'forty-ninth-meng-huo-returns', background: 'story-nanzhong-captures', expected: 'portrait-meng-huo-campaign' },
|
||||
{ key: 'meng-huo', id: 'fifty-fourth-victory-meng-huo-yields', background: 'story-nanzhong-captures', expected: 'portrait-meng-huo-final' }
|
||||
];
|
||||
|
||||
['liuBei', 'guanYu', 'zhangFei', 'zhaoYun', 'jiangWei', 'cao-cao', 'simaYi', 'meng-huo'].forEach((key) => {
|
||||
const routeCaseCount = contextualPortraitRouteCases.filter((testCase) => testCase.key === key).length;
|
||||
if (routeCaseCount < 3) {
|
||||
errors.push(`portrait era route coverage: ${key} requires early/middle/late assertions, received ${routeCaseCount}`);
|
||||
}
|
||||
});
|
||||
|
||||
contextualPortraitRouteCases.forEach(({ key, id, background, expected }) => {
|
||||
const storyContext = { id, background };
|
||||
const candidates = portraitAssetEntriesForStoryContext(key, storyContext);
|
||||
const selected = portraitAssetEntryForStoryContext(key, storyContext);
|
||||
if (candidates.length !== 1 || candidates[0].textureKey !== expected) {
|
||||
errors.push(
|
||||
`portrait era route: ${key}/${id} should expose only "${expected}", received ` +
|
||||
`[${candidates.map(({ textureKey }) => textureKey).join(', ')}]`
|
||||
);
|
||||
}
|
||||
if (selected?.textureKey !== expected) {
|
||||
errors.push(
|
||||
`portrait era selection: ${key}/${id} should select "${expected}", received "${selected?.textureKey ?? 'none'}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const zhugeLiangBaseRevisionFixture = 'zhuge-liang-v2';
|
||||
const previousBaseRevision = portraitAssets[zhugeLiangBaseRevisionFixture];
|
||||
portraitAssets[zhugeLiangBaseRevisionFixture] = `virtual:${zhugeLiangBaseRevisionFixture}`;
|
||||
@@ -511,7 +587,16 @@ function validateRepeatedStoryBackgroundDistribution(errors, selectedBackgroundU
|
||||
}
|
||||
|
||||
function validatePortrait(context, pageContext) {
|
||||
const { errors, page, usedPortraitKeys, portraitAssetEntriesForStoryContext, portraitKeyForSpeaker } = context;
|
||||
const {
|
||||
errors,
|
||||
page,
|
||||
usedPortraitKeys,
|
||||
portraitAssetEntryForStoryContext,
|
||||
portraitAssetEntriesForStoryContext,
|
||||
portraitKeyForSpeaker,
|
||||
portraitSelectionMetrics
|
||||
} = context;
|
||||
portraitSelectionMetrics.pages += 1;
|
||||
const portraitKey = page.portrait ?? portraitKeyForSpeaker(page.speaker);
|
||||
if (!portraitKey) {
|
||||
return;
|
||||
@@ -520,6 +605,20 @@ function validatePortrait(context, pageContext) {
|
||||
const entries = portraitAssetEntriesForStoryContext(portraitKey, page);
|
||||
if (entries.length === 0) {
|
||||
errors.push(`${pageContext}: missing portrait asset "${portraitKey}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEntry = portraitAssetEntryForStoryContext(portraitKey, page);
|
||||
if (!selectedEntry) {
|
||||
errors.push(`${pageContext}: story portrait selector returned no entry for "${portraitKey}"`);
|
||||
} else if (!entries.some(({ textureKey, url }) =>
|
||||
textureKey === selectedEntry.textureKey && url === selectedEntry.url
|
||||
)) {
|
||||
errors.push(
|
||||
`${pageContext}: selected portrait "${selectedEntry.textureKey}" is not a contextual candidate for "${portraitKey}"`
|
||||
);
|
||||
} else {
|
||||
portraitSelectionMetrics.dialoguePortraits += 1;
|
||||
}
|
||||
usedPortraitKeys.add(portraitKey);
|
||||
}
|
||||
@@ -551,7 +650,11 @@ function validateCutsceneActor(context, pageContext, actor, actorIndex) {
|
||||
usedCutsceneActorIds,
|
||||
usedPortraitKeys,
|
||||
storyCutsceneActorProfileFor,
|
||||
portraitAssetEntryForStoryContext,
|
||||
portraitAssetEntriesForKey,
|
||||
portraitAssetEntriesForStoryContext,
|
||||
portraitSelectionMetrics,
|
||||
page,
|
||||
hasUnitSheetAsset
|
||||
} = context;
|
||||
const actorContext = `${pageContext}/actor[${actorIndex}]`;
|
||||
@@ -589,8 +692,25 @@ function validateCutsceneActor(context, pageContext, actor, actorIndex) {
|
||||
|
||||
if (!portraitlessActorIds.has(actor.unitId)) {
|
||||
assertNonEmptyString(errors, profile.portraitKey, `${actorContext}: actor profile portrait key`);
|
||||
if (portraitAssetEntriesForKey(profile.portraitKey).length === 0) {
|
||||
const allEntries = portraitAssetEntriesForKey(profile.portraitKey);
|
||||
if (allEntries.length === 0) {
|
||||
errors.push(`${actorContext}: missing actor portrait "${profile.portraitKey}"`);
|
||||
return;
|
||||
}
|
||||
const contextualEntries = portraitAssetEntriesForStoryContext(profile.portraitKey, page);
|
||||
const selectedEntry = portraitAssetEntryForStoryContext(
|
||||
profile.portraitKey,
|
||||
page,
|
||||
`${page.id}:${actor.unitId}:${actorIndex}`
|
||||
);
|
||||
if (!selectedEntry) {
|
||||
errors.push(`${actorContext}: contextual actor portrait selector returned no entry`);
|
||||
} else if (!contextualEntries.some(({ textureKey, url }) =>
|
||||
textureKey === selectedEntry.textureKey && url === selectedEntry.url
|
||||
)) {
|
||||
errors.push(`${actorContext}: selected actor portrait "${selectedEntry.textureKey}" is not a contextual candidate`);
|
||||
} else {
|
||||
portraitSelectionMetrics.cutsceneActorPortraits += 1;
|
||||
}
|
||||
usedPortraitKeys.add(profile.portraitKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user