diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index 60570eb..a88e6b5 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -45,9 +45,11 @@ try { const normalizedLaunchSupplies = normalizeCampaignSortieItemAssignments( { - 'liu-bei': { bean: '9', salve: '2', stone: '4' }, + 'liu-bei': { ' bean ': '9', salve: '2', stone: '4', ['x'.repeat(97)]: '1' }, + ' liu-bei ': { wine: '1' }, 'guan-yu': { wine: '3' }, 'ghost-unit': { bean: '1' }, + ['x'.repeat(97)]: { bean: '1' }, '': { bean: '1' } }, new Set(['liu-bei', 'guan-yu']) @@ -56,10 +58,21 @@ try { normalizedLaunchSupplies['liu-bei']?.bean === 2 && normalizedLaunchSupplies['liu-bei']?.salve === 1 && normalizedLaunchSupplies['liu-bei']?.stone === undefined && + normalizedLaunchSupplies['liu-bei']?.wine === 1 && + normalizedLaunchSupplies['liu-bei']?.['x'.repeat(97)] === undefined && normalizedLaunchSupplies['guan-yu']?.wine === 1 && normalizedLaunchSupplies['ghost-unit'] === undefined, `Expected launch sortie supplies to share save normalization rules: ${JSON.stringify(normalizedLaunchSupplies)}` ); + const cappedLaunchSupplies = normalizeCampaignSortieItemAssignments( + Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, { bean: 1 }])) + ); + assert( + Object.keys(cappedLaunchSupplies).length === 96 && + cappedLaunchSupplies['unit-95']?.bean === 1 && + cappedLaunchSupplies['unit-96'] === undefined, + `Expected launch sortie supplies without a roster filter to stay capped: ${JSON.stringify(cappedLaunchSupplies)}` + ); resetCampaignState(); const firstScenario = battleScenarios['first-battle-zhuo-commandery']; @@ -222,9 +235,10 @@ try { selectedSortieUnitIds: { corrupted: true }, sortieFormationAssignments: 'front', sortieItemAssignments: { - 'liu-bei': { bean: '5', wine: -1, salve: '1', stone: '9' }, + 'liu-bei': { ' bean ': '5', wine: -1, salve: '1', stone: '9', ['x'.repeat(97)]: '1' }, 'guan-yu': '2', 'zhang-fei': ['bean', '2'], + ['x'.repeat(97)]: { bean: 1 }, '': { bean: 4 } }, battleHistory: [] @@ -260,8 +274,10 @@ try { malformed.sortieItemAssignments['liu-bei']?.salve === 1 && malformed.sortieItemAssignments['liu-bei']?.stone === undefined && malformed.sortieItemAssignments['liu-bei']?.wine === undefined && + malformed.sortieItemAssignments['liu-bei']?.['x'.repeat(97)] === undefined && malformed.sortieItemAssignments['guan-yu'] === undefined && malformed.sortieItemAssignments['zhang-fei'] === undefined && + malformed.sortieItemAssignments['x'.repeat(97)] === undefined && Object.keys(malformed.sortieItemAssignments).length === 1 && Object.keys(malformed.sortieFormationAssignments).length === 0, `Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}` diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 295c80e..c2ea90b 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -274,6 +274,7 @@ const maxCampaignBattleObjectiveEntries = 24; const maxCampaignBattleUnitEntries = 96; const maxCampaignBattleBondEntries = 96; const maxCampaignReserveTrainingEntries = 96; +const maxCampaignSortieAssignmentUnits = 96; const maxCampaignDisplayTextLength = 180; export const campaignReserveTrainingFocusDefinitions: CampaignReserveTrainingFocusDefinition[] = [ @@ -1176,24 +1177,33 @@ function normalizeReserveTrainingSnapshot(value: unknown): CampaignReserveTraini export function normalizeCampaignSortieItemAssignments(assignments?: Record, allowedUnitIds?: Set) { return Object.entries(assignments ?? {}).reduce((next, [unitId, stocks]) => { - if (!unitId || (allowedUnitIds && !allowedUnitIds.has(unitId))) { + const normalizedUnitId = normalizeKeyString(unitId); + if ( + !normalizedUnitId || + (allowedUnitIds && !allowedUnitIds.has(normalizedUnitId)) || + (!allowedUnitIds && Object.keys(next).length >= maxCampaignSortieAssignmentUnits) + ) { return next; } const unitStocks = Object.entries(recordOrEmpty(stocks)).reduce>((nextStocks, [itemId, amount]) => { - const maxPerUnit = sortieSupplyMaxByUsableId[itemId]; + const normalizedItemId = normalizeKeyString(itemId); + const maxPerUnit = sortieSupplyMaxByUsableId[normalizedItemId]; if (!maxPerUnit) { return nextStocks; } const normalizedAmount = normalizeNonNegativeInteger(amount); - if (itemId && normalizedAmount > 0) { - nextStocks[itemId] = Math.min(maxPerUnit, normalizedAmount); + if (normalizedItemId && normalizedAmount > 0) { + nextStocks[normalizedItemId] = Math.min(maxPerUnit, normalizedAmount); } return nextStocks; }, {}); if (Object.keys(unitStocks).length > 0) { - next[unitId] = unitStocks; + next[normalizedUnitId] = { + ...(next[normalizedUnitId] ?? {}), + ...unitStocks + }; } return next; }, {});