diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index a88e6b5..6b89c35 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -38,6 +38,7 @@ try { setFirstBattleReport } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); + const { normalizeSortieFormationAssignments } = await server.ssrLoadModule('/src/game/data/sortieDeployment.ts'); storage.clear(); const empty = loadCampaignState(); @@ -73,6 +74,35 @@ try { cappedLaunchSupplies['unit-96'] === undefined, `Expected launch sortie supplies without a roster filter to stay capped: ${JSON.stringify(cappedLaunchSupplies)}` ); + const normalizedLaunchFormation = normalizeSortieFormationAssignments( + { + 'liu-bei': 'front', + ' liu-bei ': 'support', + 'guan-yu': 'flank', + 'ghost-unit': 'reserve', + 'zhang-fei': 'broken', + ['x'.repeat(97)]: 'front', + '': 'front' + }, + new Set(['liu-bei', 'guan-yu']) + ); + assert( + normalizedLaunchFormation['liu-bei'] === 'front' && + normalizedLaunchFormation['guan-yu'] === 'flank' && + normalizedLaunchFormation['ghost-unit'] === undefined && + normalizedLaunchFormation['zhang-fei'] === undefined && + normalizedLaunchFormation['x'.repeat(97)] === undefined, + `Expected launch sortie formation assignments to trim, filter, and preserve first valid roles: ${JSON.stringify(normalizedLaunchFormation)}` + ); + const cappedLaunchFormation = normalizeSortieFormationAssignments( + Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'front'])) + ); + assert( + Object.keys(cappedLaunchFormation).length === 96 && + cappedLaunchFormation['unit-95'] === 'front' && + cappedLaunchFormation['unit-96'] === undefined, + `Expected launch sortie formation assignments without a roster filter to stay capped: ${JSON.stringify(cappedLaunchFormation)}` + ); resetCampaignState(); const firstScenario = battleScenarios['first-battle-zhuo-commandery']; diff --git a/src/game/data/sortieDeployment.ts b/src/game/data/sortieDeployment.ts index e09c0d6..98e9d49 100644 --- a/src/game/data/sortieDeployment.ts +++ b/src/game/data/sortieDeployment.ts @@ -5,6 +5,9 @@ export type SortieFormationAssignments = Partial, allowedUnitIds?: Set ): SortieFormationAssignments { - return Object.fromEntries( - Object.entries(assignments ?? {}).filter(([unitId, role]) => { - return (!allowedUnitIds || allowedUnitIds.has(unitId)) && isSortieFormationRole(role); - }) - ) as SortieFormationAssignments; + return Object.entries(assignments ?? {}).reduce((next, [unitId, role]) => { + const normalizedUnitId = normalizeSortieFormationUnitId(unitId); + if ( + !normalizedUnitId || + next[normalizedUnitId] || + (allowedUnitIds && !allowedUnitIds.has(normalizedUnitId)) || + (!allowedUnitIds && Object.keys(next).length >= maxSortieFormationAssignmentUnits) || + !isSortieFormationRole(role) + ) { + return next; + } + + next[normalizedUnitId] = role; + return next; + }, {}); +} + +function normalizeSortieFormationUnitId(value: unknown) { + if (typeof value !== 'string') { + return ''; + } + + const text = value.trim(); + return text.length > 0 && text.length <= maxSortieFormationUnitIdLength ? text : ''; } export function createSortieDeploymentPlan(