feat: improve first-session combat and visual assets

This commit is contained in:
2026-07-18 22:11:50 +09:00
parent 4a2a0dfb19
commit f29956b90e
82 changed files with 3640 additions and 839 deletions

View File

@@ -1,3 +1,5 @@
import { readdirSync } from 'node:fs';
import { join } from 'node:path';
import { createServer } from 'vite';
const validCutsceneKinds = new Set(['muster', 'operation', 'victory']);
@@ -15,7 +17,8 @@ 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, storyBackgroundKeysFor } = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
const { storyBackgroundAssets, storyBackgroundKeyForPage, storyBackgroundKeysFor } =
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');
@@ -24,6 +27,8 @@ try {
const storySources = collectStorySources(scenarioModule, battleScenarios);
const uniquePageIds = new Set();
const usedBackgroundKeys = new Set();
const selectedBackgroundKeys = new Set();
const selectedBackgroundUrlsByBase = new Map();
const usedPortraitKeys = new Set();
const usedCutsceneActorIds = new Set();
let pageCount = 0;
@@ -44,9 +49,12 @@ try {
index,
page,
usedBackgroundKeys,
selectedBackgroundKeys,
selectedBackgroundUrlsByBase,
usedPortraitKeys,
usedCutsceneActorIds,
storyBackgroundAssets,
storyBackgroundKeyForPage,
storyBackgroundKeysFor,
portraitAssetEntriesForKey,
portraitKeyForSpeaker,
@@ -59,6 +67,9 @@ try {
});
});
validateStoryBackgroundVariantCoverage(errors, selectedBackgroundKeys);
validateRepeatedStoryBackgroundDistribution(errors, selectedBackgroundUrlsByBase);
if (errors.length) {
console.error(`Story asset data verification failed with ${errors.length} issue(s):`);
errors.slice(0, 100).forEach((error) => console.error(`- ${error}`));
@@ -69,7 +80,8 @@ try {
} else {
console.log(
`Verified ${pageCount} story page references (${uniquePageIds.size} unique pages), ` +
`${cutsceneCount} cutscenes, ${usedBackgroundKeys.size} backgrounds, ` +
`${cutsceneCount} cutscenes, ${usedBackgroundKeys.size} background candidates, ` +
`${selectedBackgroundKeys.size} selected backgrounds, ` +
`${usedPortraitKeys.size} portrait keys, and ${usedCutsceneActorIds.size} cutscene actor profiles.`
);
}
@@ -131,7 +143,16 @@ function validateStoryPage(context) {
}
function validateBackground(context, pageContext) {
const { errors, page, usedBackgroundKeys, storyBackgroundAssets, storyBackgroundKeysFor } = context;
const {
errors,
page,
usedBackgroundKeys,
selectedBackgroundKeys,
selectedBackgroundUrlsByBase,
storyBackgroundAssets,
storyBackgroundKeyForPage,
storyBackgroundKeysFor
} = context;
if (!page.background) {
return;
}
@@ -139,8 +160,71 @@ function validateBackground(context, pageContext) {
const keys = storyBackgroundKeysFor(page.background).filter((key) => storyBackgroundAssets[key]);
if (keys.length === 0) {
errors.push(`${pageContext}: missing story background asset "${page.background}"`);
return;
}
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;
}
const selectedUrl = storyBackgroundAssets[selectedKey];
if (!selectedUrl) {
errors.push(`${pageContext}: selected background "${selectedKey}" has no runtime asset URL`);
return;
}
selectedBackgroundKeys.add(selectedKey);
const backgroundGroup = selectedBackgroundUrlsByBase.get(page.background) ?? {
pageCount: 0,
candidateUrls: new Set(),
selectedUrls: new Set()
};
backgroundGroup.pageCount += 1;
keys.forEach((key) => backgroundGroup.candidateUrls.add(storyBackgroundAssets[key]));
backgroundGroup.selectedUrls.add(selectedUrl);
selectedBackgroundUrlsByBase.set(page.background, backgroundGroup);
}
function validateStoryBackgroundVariantCoverage(errors, selectedBackgroundKeys) {
const firstVariantNumber = 33;
const lastVariantNumber = 134;
const expectedVariantCount = lastVariantNumber - firstVariantNumber + 1;
const variantKeys = readdirSync(join('src', 'assets', 'images', 'story'), { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name.match(/^(\d+)-(.+)\.png$/i))
.filter((match) => match && Number(match[1]) >= firstVariantNumber && Number(match[1]) <= lastVariantNumber)
.map((match) => `story-${match[2]}`)
.sort();
if (variantKeys.length !== expectedVariantCount) {
errors.push(
`story background variants ${firstVariantNumber}-${lastVariantNumber}: expected ${expectedVariantCount} files, ` +
`found ${variantKeys.length}`
);
}
variantKeys.forEach((key) => {
if (!selectedBackgroundKeys.has(key)) {
errors.push(`story background variant "${key}" is never selected by a campaign page`);
}
});
}
function validateRepeatedStoryBackgroundDistribution(errors, selectedBackgroundUrlsByBase) {
selectedBackgroundUrlsByBase.forEach(({ pageCount, candidateUrls, selectedUrls }, baseKey) => {
if (pageCount > 1 && candidateUrls.size > 1 && selectedUrls.size < 2) {
errors.push(
`${baseKey}: ${pageCount} repeated pages should use at least two of ${candidateUrls.size} background variants`
);
}
});
}
function validatePortrait(context, pageContext) {