226 lines
9.0 KiB
JavaScript
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);
|
|
}
|