feat: enrich campaign reward and roster visuals
This commit is contained in:
@@ -7,6 +7,8 @@ const validMarkerSides = new Set(['ally', 'enemy', 'objective']);
|
||||
const validRewardTones = new Set(['reward', 'next', 'bond']);
|
||||
const validDirections = new Set(['south', 'east', 'north', 'west']);
|
||||
const portraitlessActorIds = new Set(['rebel-leader']);
|
||||
const maxAdjacentBackgroundDuplicateRate = 0.02;
|
||||
const maxAdjacentBackgroundRun = 3;
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
@@ -17,8 +19,12 @@ const server = await createServer({
|
||||
try {
|
||||
const scenarioModule = await server.ssrLoadModule('/src/game/data/scenario.ts');
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
const { storyBackgroundAssets, storyBackgroundKeyForPage, storyBackgroundKeysFor } =
|
||||
await server.ssrLoadModule('/src/game/data/storyAssets.ts');
|
||||
const {
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
storyBackgroundKeysForPages,
|
||||
storyBackgroundPageHasFixedAssignment
|
||||
} = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
|
||||
const { portraitAssetEntriesForKey, 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');
|
||||
@@ -31,6 +37,12 @@ try {
|
||||
const selectedBackgroundUrlsByBase = new Map();
|
||||
const usedPortraitKeys = new Set();
|
||||
const usedCutsceneActorIds = new Set();
|
||||
const adjacencyMetrics = {
|
||||
pairs: 0,
|
||||
duplicatePairs: 0,
|
||||
avoidableDuplicatePairs: 0,
|
||||
maxRun: 0
|
||||
};
|
||||
let pageCount = 0;
|
||||
let cutsceneCount = 0;
|
||||
|
||||
@@ -40,6 +52,23 @@ try {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedKeys = storyBackgroundKeysForPages(pages);
|
||||
const repeatedSelection = storyBackgroundKeysForPages(pages);
|
||||
if (selectedKeys.some((key, index) => key !== repeatedSelection[index])) {
|
||||
errors.push(`${source}: story background sequence selection must be deterministic`);
|
||||
}
|
||||
|
||||
validateStoryBackgroundSequence({
|
||||
errors,
|
||||
source,
|
||||
pages,
|
||||
selectedKeys,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
storyBackgroundPageHasFixedAssignment,
|
||||
metrics: adjacencyMetrics
|
||||
});
|
||||
|
||||
pages.forEach((page, index) => {
|
||||
pageCount += 1;
|
||||
uniquePageIds.add(page.id);
|
||||
@@ -48,13 +77,13 @@ try {
|
||||
source,
|
||||
index,
|
||||
page,
|
||||
selectedKey: selectedKeys[index],
|
||||
usedBackgroundKeys,
|
||||
selectedBackgroundKeys,
|
||||
selectedBackgroundUrlsByBase,
|
||||
usedPortraitKeys,
|
||||
usedCutsceneActorIds,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeyForPage,
|
||||
storyBackgroundKeysFor,
|
||||
portraitAssetEntriesForKey,
|
||||
portraitKeyForSpeaker,
|
||||
@@ -69,6 +98,7 @@ try {
|
||||
|
||||
validateStoryBackgroundVariantCoverage(errors, selectedBackgroundKeys);
|
||||
validateRepeatedStoryBackgroundDistribution(errors, selectedBackgroundUrlsByBase);
|
||||
validateStoryBackgroundAdjacencyMetrics(errors, adjacencyMetrics);
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`Story asset data verification failed with ${errors.length} issue(s):`);
|
||||
@@ -78,11 +108,16 @@ try {
|
||||
}
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
const adjacentDuplicateRate = adjacencyMetrics.pairs > 0
|
||||
? adjacencyMetrics.duplicatePairs / adjacencyMetrics.pairs
|
||||
: 0;
|
||||
console.log(
|
||||
`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, and ${usedCutsceneActorIds.size} cutscene actor profiles; ` +
|
||||
`${adjacencyMetrics.duplicatePairs}/${adjacencyMetrics.pairs} adjacent background repeats ` +
|
||||
`(${(adjacentDuplicateRate * 100).toFixed(1)}%), max run ${adjacencyMetrics.maxRun}.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -146,11 +181,11 @@ function validateBackground(context, pageContext) {
|
||||
const {
|
||||
errors,
|
||||
page,
|
||||
selectedKey,
|
||||
usedBackgroundKeys,
|
||||
selectedBackgroundKeys,
|
||||
selectedBackgroundUrlsByBase,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeyForPage,
|
||||
storyBackgroundKeysFor
|
||||
} = context;
|
||||
if (!page.background) {
|
||||
@@ -164,11 +199,6 @@ function validateBackground(context, pageContext) {
|
||||
}
|
||||
keys.forEach((key) => usedBackgroundKeys.add(key));
|
||||
|
||||
const selectedKey = storyBackgroundKeyForPage(page.background, page.id);
|
||||
const repeatedSelection = storyBackgroundKeyForPage(page.background, page.id);
|
||||
if (selectedKey !== repeatedSelection) {
|
||||
errors.push(`${pageContext}: story background selection must be deterministic`);
|
||||
}
|
||||
if (!keys.includes(selectedKey)) {
|
||||
errors.push(`${pageContext}: selected background "${selectedKey}" is not a candidate for "${page.background}"`);
|
||||
return;
|
||||
@@ -192,6 +222,98 @@ function validateBackground(context, pageContext) {
|
||||
selectedBackgroundUrlsByBase.set(page.background, backgroundGroup);
|
||||
}
|
||||
|
||||
function validateStoryBackgroundSequence(context) {
|
||||
const {
|
||||
errors,
|
||||
source,
|
||||
pages,
|
||||
selectedKeys,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
storyBackgroundPageHasFixedAssignment,
|
||||
metrics
|
||||
} = context;
|
||||
let previousUrl;
|
||||
let previousPage;
|
||||
let previousKey;
|
||||
let currentRun = 0;
|
||||
|
||||
pages.forEach((page, index) => {
|
||||
const selectedKey = selectedKeys[index];
|
||||
const selectedUrl = storyBackgroundAssets[selectedKey] ?? selectedKey;
|
||||
if (index > 0) {
|
||||
metrics.pairs += 1;
|
||||
}
|
||||
|
||||
if (previousUrl !== undefined && selectedUrl === previousUrl) {
|
||||
metrics.duplicatePairs += 1;
|
||||
currentRun += 1;
|
||||
|
||||
const previousUrls = selectableStoryBackgroundUrls(
|
||||
previousPage,
|
||||
previousKey,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
storyBackgroundPageHasFixedAssignment
|
||||
);
|
||||
const currentUrls = selectableStoryBackgroundUrls(
|
||||
page,
|
||||
selectedKey,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
storyBackgroundPageHasFixedAssignment
|
||||
);
|
||||
const hasDistinctPair = [...previousUrls].some((leftUrl) =>
|
||||
[...currentUrls].some((rightUrl) => leftUrl !== rightUrl)
|
||||
);
|
||||
if (hasDistinctPair) {
|
||||
metrics.avoidableDuplicatePairs += 1;
|
||||
errors.push(`${source}[${index}]/${page.id}: avoidable adjacent story background repeat`);
|
||||
}
|
||||
} else {
|
||||
currentRun = 1;
|
||||
}
|
||||
|
||||
metrics.maxRun = Math.max(metrics.maxRun, currentRun);
|
||||
previousUrl = selectedUrl;
|
||||
previousPage = page;
|
||||
previousKey = selectedKey;
|
||||
});
|
||||
}
|
||||
|
||||
function selectableStoryBackgroundUrls(
|
||||
page,
|
||||
selectedKey,
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeysFor,
|
||||
storyBackgroundPageHasFixedAssignment
|
||||
) {
|
||||
const selectedUrl = storyBackgroundAssets[selectedKey] ?? selectedKey;
|
||||
if (!page || storyBackgroundPageHasFixedAssignment(page.id)) {
|
||||
return new Set([selectedUrl]);
|
||||
}
|
||||
return new Set(
|
||||
storyBackgroundKeysFor(page.background)
|
||||
.map((key) => storyBackgroundAssets[key] ?? key)
|
||||
);
|
||||
}
|
||||
|
||||
function validateStoryBackgroundAdjacencyMetrics(errors, metrics) {
|
||||
const duplicateRate = metrics.pairs > 0 ? metrics.duplicatePairs / metrics.pairs : 0;
|
||||
if (metrics.avoidableDuplicatePairs > 0) {
|
||||
errors.push(`${metrics.avoidableDuplicatePairs} avoidable adjacent story background repeat(s) remain`);
|
||||
}
|
||||
if (duplicateRate > maxAdjacentBackgroundDuplicateRate) {
|
||||
errors.push(
|
||||
`adjacent story background duplicate rate ${(duplicateRate * 100).toFixed(1)}% exceeds ` +
|
||||
`${(maxAdjacentBackgroundDuplicateRate * 100).toFixed(1)}%`
|
||||
);
|
||||
}
|
||||
if (metrics.maxRun > maxAdjacentBackgroundRun) {
|
||||
errors.push(`story background max run ${metrics.maxRun} exceeds ${maxAdjacentBackgroundRun}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateStoryBackgroundVariantCoverage(errors, selectedBackgroundKeys) {
|
||||
const firstVariantNumber = 33;
|
||||
const lastVariantNumber = 134;
|
||||
|
||||
Reference in New Issue
Block a user