Files
heros_web/scripts/verify-camp-skin-data.mjs

226 lines
9.0 KiB
JavaScript

import { createServer } from 'vite';
const expectedArcs = [
{ skinId: 'yellow-turban', firstBattleOrdinal: 1, lastBattleOrdinal: 4 },
{ skinId: 'anti-dong', firstBattleOrdinal: 5, lastBattleOrdinal: 6 },
{ skinId: 'xuzhou', firstBattleOrdinal: 7, lastBattleOrdinal: 9 },
{ skinId: 'wandering', firstBattleOrdinal: 10, lastBattleOrdinal: 15 },
{ skinId: 'wolong', firstBattleOrdinal: 16, lastBattleOrdinal: 18 },
{ skinId: 'red-cliffs', firstBattleOrdinal: 19, lastBattleOrdinal: 22 },
{ skinId: 'jing-yi', firstBattleOrdinal: 23, lastBattleOrdinal: 34 },
{ skinId: 'hanzhong-shuhan', firstBattleOrdinal: 35, lastBattleOrdinal: 37 },
{ skinId: 'jingzhou-crisis', firstBattleOrdinal: 38, lastBattleOrdinal: 44 },
{ skinId: 'yiling-baidi', firstBattleOrdinal: 45, lastBattleOrdinal: 46 },
{ skinId: 'nanzhong', firstBattleOrdinal: 47, lastBattleOrdinal: 54 },
{ skinId: 'northern', firstBattleOrdinal: 55, lastBattleOrdinal: 66 }
];
const expectedTextureKeys = {
'yellow-turban': 'story-militia-training-yard',
'anti-dong': 'story-anti-dong-coalition-oath-camp',
xuzhou: 'story-xuzhou-gate-council',
wandering: 'story-wandering-night-camp',
wolong: 'story-wolong-autumn-study',
'red-cliffs': 'story-red-cliffs-wind-altar',
'jing-yi': 'story-yizhou-council-at-river',
'hanzhong-shuhan': 'story-yizhou-mountain-pass-command-post',
'jingzhou-crisis': 'story-jingzhou-crisis-river-watchtowers',
'yiling-baidi': 'story-yiling-baidi-lantern-vigil',
nanzhong: 'story-nanzhong-captures-river-night-camp',
northern: 'story-northern-supply-depot'
};
const expectedSpecialSteps = {
'hanzhong-king-camp': 'hanzhong-shuhan',
'shu-han-foundation-camp': 'hanzhong-shuhan',
'baidi-entrustment-camp': 'yiling-baidi',
'northern-campaign-prep-camp': 'northern',
'ending-complete': 'northern'
};
const errors = [];
const server = await createServer({
logLevel: 'error',
server: { middlewareMode: true },
appType: 'custom'
});
try {
const { campSkinDefinitions, campSkinBattleArcs, campSkinBattleOrdinal, selectCampSkin } =
await server.ssrLoadModule('/src/game/data/campSkins.ts');
const { storyBackgroundAssets } = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
const expectedSkinIds = expectedArcs.map(({ skinId }) => skinId);
const definitionEntries = Object.entries(campSkinDefinitions);
const battleIds = Object.keys(battleScenarios);
assertEqual(definitionEntries.length, expectedSkinIds.length, 'camp skin definition count');
assertEqual(campSkinBattleArcs.length, expectedArcs.length, 'camp skin arc count');
assertEqual(battleIds.length, 66, 'battle count used by camp skin ordinals');
expectedSkinIds.forEach((skinId) => {
const definition = campSkinDefinitions[skinId];
assert(Boolean(definition), `missing camp skin definition "${skinId}"`);
if (!definition) {
return;
}
assertEqual(definition.id, skinId, `${skinId}: id`);
assertEqual(definition.textureKey, expectedTextureKeys[skinId], `${skinId}: texture key`);
['textureKey', 'assetUrl', 'chapterLabel', 'locationLabel', 'seal'].forEach((fieldName) => {
assertNonEmptyString(definition[fieldName], `${skinId}: ${fieldName}`);
});
['accentColor', 'headerColor', 'washColor'].forEach((fieldName) => {
assertColor(definition[fieldName], `${skinId}: ${fieldName}`);
});
assertAlpha(definition.backdropAlpha, `${skinId}: backdropAlpha`);
assertAlpha(definition.washAlpha, `${skinId}: washAlpha`);
assert(
Object.hasOwn(storyBackgroundAssets, definition.textureKey),
`${skinId}: missing story background "${definition.textureKey}"`
);
assertEqual(
definition.assetUrl,
storyBackgroundAssets[definition.textureKey],
`${skinId}: asset URL must match its story background`
);
});
definitionEntries.forEach(([skinId]) => {
assert(expectedSkinIds.includes(skinId), `unexpected camp skin definition "${skinId}"`);
});
assertUnique(
definitionEntries.map(([, definition]) => definition.textureKey),
'camp skin texture keys'
);
assertUnique(
definitionEntries.map(
([, definition]) => `${definition.accentColor}:${definition.headerColor}:${definition.washColor}`
),
'camp skin visual color signatures'
);
let nextExpectedOrdinal = 1;
expectedArcs.forEach((expectedArc, index) => {
const actualArc = campSkinBattleArcs[index];
assert(Boolean(actualArc), `missing camp skin arc at index ${index}`);
if (!actualArc) {
return;
}
assertEqual(actualArc.skinId, expectedArc.skinId, `arc ${index}: skin id`);
assertEqual(actualArc.firstBattleOrdinal, expectedArc.firstBattleOrdinal, `arc ${index}: first battle ordinal`);
assertEqual(actualArc.lastBattleOrdinal, expectedArc.lastBattleOrdinal, `arc ${index}: last battle ordinal`);
assertEqual(actualArc.firstBattleOrdinal, nextExpectedOrdinal, `arc ${index}: contiguous first battle ordinal`);
assert(
actualArc.lastBattleOrdinal >= actualArc.firstBattleOrdinal,
`arc ${index}: last battle ordinal must not precede its first`
);
nextExpectedOrdinal = actualArc.lastBattleOrdinal + 1;
});
assertEqual(nextExpectedOrdinal, battleIds.length + 1, 'camp skin arcs must cover every battle ordinal');
battleIds.forEach((battleId, index) => {
const battleOrdinal = index + 1;
const expectedSkinId = expectedSkinForOrdinal(battleOrdinal);
const nextBattleSelection = selectCampSkin({ nextBattleId: battleId });
const currentBattleSelection = selectCampSkin({ currentBattleId: battleId });
assertEqual(campSkinBattleOrdinal(battleId), battleOrdinal, `${battleId}: battle ordinal lookup`);
assertSelection(nextBattleSelection, {
skinId: expectedSkinId,
source: 'next-battle',
requestedBattleId: battleId,
battleOrdinal
});
assertSelection(currentBattleSelection, {
skinId: expectedSkinId,
source: 'current-battle',
requestedBattleId: battleId,
battleOrdinal
});
});
Object.entries(expectedSpecialSteps).forEach(([campaignStep, skinId]) => {
const selection = selectCampSkin({ campaignStep, nextBattleId: battleIds[0], currentBattleId: battleIds.at(-1) });
assertSelection(selection, {
skinId,
source: 'special-step',
requestedBattleId: battleIds[0],
battleOrdinal: 1
});
});
const currentBattleRecovery = selectCampSkin({ nextBattleId: 'unknown-battle', currentBattleId: battleIds.at(-1) });
assertSelection(currentBattleRecovery, {
skinId: 'northern',
source: 'current-battle',
requestedBattleId: battleIds.at(-1),
battleOrdinal: battleIds.length
});
assertSelection(selectCampSkin({ nextBattleId: 'unknown-battle' }), {
skinId: 'yellow-turban',
source: 'fallback'
});
assertEqual(campSkinBattleOrdinal('unknown-battle'), undefined, 'unknown battle ordinal lookup');
if (errors.length > 0) {
throw new Error(`Camp skin data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`);
}
console.log(
`Verified ${definitionEntries.length} camp skins, ${campSkinBattleArcs.length} contiguous era arcs, ` +
`${battleIds.length} next/current battle selections, and ${Object.keys(expectedSpecialSteps).length} special-step overrides.`
);
} finally {
await server.close();
}
function expectedSkinForOrdinal(battleOrdinal) {
return expectedArcs.find(
({ firstBattleOrdinal, lastBattleOrdinal }) =>
battleOrdinal >= firstBattleOrdinal && battleOrdinal <= lastBattleOrdinal
)?.skinId;
}
function assertSelection(selection, expected) {
const context = `${expected.source}/${expected.requestedBattleId ?? expected.skinId}`;
assert(Boolean(selection), `${context}: selection is missing`);
if (!selection) {
return;
}
assertEqual(selection.skin?.id, expected.skinId, `${context}: selected skin`);
assertEqual(selection.source, expected.source, `${context}: selection source`);
assertEqual(selection.requestedBattleId, expected.requestedBattleId, `${context}: requested battle id`);
assertEqual(selection.battleOrdinal, expected.battleOrdinal, `${context}: battle ordinal`);
}
function assertNonEmptyString(value, context) {
assert(typeof value === 'string' && value.trim().length > 0, `${context} must be a non-empty string`);
}
function assertColor(value, context) {
assert(Number.isInteger(value) && value >= 0 && value <= 0xffffff, `${context} must be a 24-bit integer color`);
}
function assertAlpha(value, context) {
assert(Number.isFinite(value) && value > 0 && value <= 1, `${context} must be greater than 0 and at most 1`);
}
function assertUnique(values, context) {
assertEqual(new Set(values).size, values.length, `${context} must be unique`);
}
function assertEqual(actual, expected, context) {
assert(Object.is(actual, expected), `${context}: expected ${format(expected)}, received ${format(actual)}`);
}
function assert(condition, message) {
if (!condition) {
errors.push(message);
}
}
function format(value) {
return value === undefined ? 'undefined' : JSON.stringify(value);
}