feat: add era-based camp skins
This commit is contained in:
225
scripts/verify-camp-skin-data.mjs
Normal file
225
scripts/verify-camp-skin-data.mjs
Normal file
@@ -0,0 +1,225 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user