feat: add sortie formation presets
This commit is contained in:
@@ -33,8 +33,10 @@ try {
|
||||
hasCampaignSave,
|
||||
loadCampaignState,
|
||||
normalizeCampaignReserveTrainingAssignments,
|
||||
normalizeCampaignSortieFormationPresets,
|
||||
normalizeCampaignSortieItemAssignments,
|
||||
resetCampaignState,
|
||||
saveCampaignState,
|
||||
setCampaignState,
|
||||
setCampaignReserveTrainingFocus,
|
||||
setCampaignReserveTrainingAssignment,
|
||||
@@ -107,6 +109,96 @@ try {
|
||||
cappedLaunchFormation['unit-96'] === undefined,
|
||||
`Expected launch sortie formation assignments without a roster filter to stay capped: ${JSON.stringify(cappedLaunchFormation)}`
|
||||
);
|
||||
const normalizedSortiePresets = normalizeCampaignSortieFormationPresets(
|
||||
{
|
||||
elite: {
|
||||
selectedUnitIds: [' liu-bei ', 'guan-yu', 'liu-bei', 'ghost-unit', '', 7, 'x'.repeat(97)],
|
||||
formationAssignments: {
|
||||
' liu-bei ': 'front',
|
||||
'guan-yu': 'flank',
|
||||
'zhang-fei': 'reserve',
|
||||
'ghost-unit': 'support',
|
||||
broken: 'center'
|
||||
},
|
||||
sortieItemAssignments: { 'liu-bei': { bean: 2 } },
|
||||
itemAssignments: { 'guan-yu': { wine: 1 } }
|
||||
},
|
||||
mobile: 'corrupted',
|
||||
siege: { selectedUnitIds: [], formationAssignments: { 'liu-bei': 'front' } },
|
||||
ghost: { selectedUnitIds: ['liu-bei'], formationAssignments: { 'liu-bei': 'front' } }
|
||||
},
|
||||
new Set(['liu-bei', 'guan-yu', 'zhang-fei'])
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(Object.keys(normalizedSortiePresets)) === JSON.stringify(['elite']) &&
|
||||
JSON.stringify(normalizedSortiePresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) &&
|
||||
JSON.stringify(normalizedSortiePresets.elite?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front', 'guan-yu': 'flank' }) &&
|
||||
!Object.prototype.hasOwnProperty.call(normalizedSortiePresets.elite ?? {}, 'sortieItemAssignments') &&
|
||||
!Object.prototype.hasOwnProperty.call(normalizedSortiePresets.elite ?? {}, 'itemAssignments'),
|
||||
`Expected sortie presets to trim and dedupe ids, whitelist slots, scope roles to selected roster members, and exclude supplies: ${JSON.stringify(normalizedSortiePresets)}`
|
||||
);
|
||||
const cappedSortiePresets = normalizeCampaignSortieFormationPresets({
|
||||
elite: {
|
||||
selectedUnitIds: Array.from({ length: 120 }, (_, index) => `unit-${index}`),
|
||||
formationAssignments: Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'front'])),
|
||||
sortieItemAssignments: { 'unit-0': { bean: 2 } }
|
||||
}
|
||||
});
|
||||
assert(
|
||||
cappedSortiePresets.elite?.selectedUnitIds.length === 96 &&
|
||||
cappedSortiePresets.elite.selectedUnitIds[0] === 'unit-0' &&
|
||||
cappedSortiePresets.elite.selectedUnitIds[95] === 'unit-95' &&
|
||||
!cappedSortiePresets.elite.selectedUnitIds.includes('unit-96') &&
|
||||
Object.keys(cappedSortiePresets.elite.formationAssignments).length === 96 &&
|
||||
cappedSortiePresets.elite.formationAssignments['unit-95'] === 'front' &&
|
||||
cappedSortiePresets.elite.formationAssignments['unit-96'] === undefined &&
|
||||
!Object.prototype.hasOwnProperty.call(cappedSortiePresets.elite, 'sortieItemAssignments'),
|
||||
`Expected each sortie preset and its role map to stay capped at 96 entries without persisting supplies: ${JSON.stringify(cappedSortiePresets)}`
|
||||
);
|
||||
|
||||
storage.clear();
|
||||
storage.set(
|
||||
campaignStorageKey,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
updatedAt: '2026-07-03T11:20:00.000Z',
|
||||
step: 'third-camp',
|
||||
activeSaveSlot: 2,
|
||||
roster: [
|
||||
{ id: 'liu-bei', name: 'Liu Bei', faction: 'ally' },
|
||||
{ id: 'guan-yu', name: 'Guan Yu', faction: 'ally' }
|
||||
],
|
||||
sortieFormationPresets: {
|
||||
elite: {
|
||||
selectedUnitIds: ['liu-bei', 'guan-yu'],
|
||||
formationAssignments: { 'liu-bei': 'front', 'guan-yu': 'flank' }
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
const loadedPresetState = loadCampaignState();
|
||||
const detachedPresetState = getCampaignState();
|
||||
detachedPresetState.sortieFormationPresets.elite.selectedUnitIds.push('ghost-unit');
|
||||
detachedPresetState.sortieFormationPresets.elite.formationAssignments['liu-bei'] = 'support';
|
||||
const unmutatedPresetState = getCampaignState();
|
||||
assert(
|
||||
JSON.stringify(unmutatedPresetState.sortieFormationPresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) &&
|
||||
unmutatedPresetState.sortieFormationPresets.elite?.formationAssignments['liu-bei'] === 'front',
|
||||
`Expected getCampaignState to deep-clone nested sortie preset members and roles: ${JSON.stringify(unmutatedPresetState.sortieFormationPresets)}`
|
||||
);
|
||||
const savedPresetState = saveCampaignState(loadedPresetState, 2);
|
||||
savedPresetState.sortieFormationPresets.elite.selectedUnitIds.length = 0;
|
||||
savedPresetState.sortieFormationPresets.elite.formationAssignments['guan-yu'] = 'support';
|
||||
const persistedCurrentPresetState = JSON.parse(storage.get(campaignStorageKey));
|
||||
const persistedSlotPresetState = JSON.parse(storage.get(`${campaignStorageKey}:slot-2`));
|
||||
assert(
|
||||
JSON.stringify(persistedCurrentPresetState.sortieFormationPresets) === JSON.stringify(persistedSlotPresetState.sortieFormationPresets) &&
|
||||
JSON.stringify(persistedCurrentPresetState.sortieFormationPresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) &&
|
||||
persistedCurrentPresetState.sortieFormationPresets.elite?.formationAssignments['liu-bei'] === 'front' &&
|
||||
persistedCurrentPresetState.sortieFormationPresets.elite?.formationAssignments['guan-yu'] === 'flank',
|
||||
`Expected saving nested sortie presets to synchronize current and slot storage without sharing returned references: ${JSON.stringify({ current: persistedCurrentPresetState.sortieFormationPresets, slot: persistedSlotPresetState.sortieFormationPresets })}`
|
||||
);
|
||||
|
||||
const normalizedReserveDrills = normalizeCampaignReserveTrainingAssignments(
|
||||
{
|
||||
'liu-bei': 'bond-practice',
|
||||
@@ -448,6 +540,8 @@ try {
|
||||
Array.isArray(legacy.selectedSortieUnitIds) &&
|
||||
typeof legacy.sortieFormationAssignments === 'object' &&
|
||||
typeof legacy.sortieItemAssignments === 'object' &&
|
||||
typeof legacy.sortieFormationPresets === 'object' &&
|
||||
Object.keys(legacy.sortieFormationPresets).length === 0 &&
|
||||
typeof legacy.reserveTrainingAssignments === 'object',
|
||||
`Expected modern sortie fields to be restored: ${JSON.stringify(legacy)}`
|
||||
);
|
||||
@@ -483,6 +577,7 @@ try {
|
||||
},
|
||||
selectedSortieUnitIds: { corrupted: true },
|
||||
sortieFormationAssignments: 'front',
|
||||
sortieFormationPresets: 'elite',
|
||||
reserveTrainingFocus: 'ghost-focus',
|
||||
reserveTrainingAssignments: {
|
||||
'liu-bei': 'bond-practice',
|
||||
@@ -534,7 +629,8 @@ try {
|
||||
malformed.sortieItemAssignments['zhang-fei'] === undefined &&
|
||||
malformed.sortieItemAssignments['x'.repeat(97)] === undefined &&
|
||||
Object.keys(malformed.sortieItemAssignments).length === 1 &&
|
||||
Object.keys(malformed.sortieFormationAssignments).length === 0,
|
||||
Object.keys(malformed.sortieFormationAssignments).length === 0 &&
|
||||
Object.keys(malformed.sortieFormationPresets).length === 0,
|
||||
`Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}`
|
||||
);
|
||||
assert(
|
||||
@@ -582,6 +678,21 @@ try {
|
||||
'guan-yu': 'support',
|
||||
broken: 'center'
|
||||
},
|
||||
sortieFormationPresets: {
|
||||
elite: {
|
||||
selectedUnitIds: ['ghost-unit', 'liu-bei', 'guan-yu', 'liu-bei'],
|
||||
formationAssignments: {
|
||||
'liu-bei': 'front',
|
||||
'ghost-unit': 'flank',
|
||||
'guan-yu': 'support'
|
||||
},
|
||||
sortieItemAssignments: { 'liu-bei': { bean: 2 } }
|
||||
},
|
||||
mobile: {
|
||||
selectedUnitIds: ['ghost-unit'],
|
||||
formationAssignments: { 'ghost-unit': 'reserve' }
|
||||
}
|
||||
},
|
||||
reserveTrainingAssignments: {
|
||||
'liu-bei': 'bond-practice',
|
||||
'ghost-unit': 'balanced',
|
||||
@@ -600,6 +711,10 @@ try {
|
||||
staleSortie.selectedSortieUnitIds[0] === 'liu-bei' &&
|
||||
staleSortie.sortieFormationAssignments['liu-bei'] === 'front' &&
|
||||
staleSortie.sortieFormationAssignments['ghost-unit'] === undefined &&
|
||||
JSON.stringify(staleSortie.sortieFormationPresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei']) &&
|
||||
JSON.stringify(staleSortie.sortieFormationPresets.elite?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) &&
|
||||
staleSortie.sortieFormationPresets.mobile === undefined &&
|
||||
!Object.prototype.hasOwnProperty.call(staleSortie.sortieFormationPresets.elite ?? {}, 'sortieItemAssignments') &&
|
||||
staleSortie.reserveTrainingAssignments['liu-bei'] === 'bond-practice' &&
|
||||
staleSortie.reserveTrainingAssignments['ghost-unit'] === undefined &&
|
||||
staleSortie.reserveTrainingAssignments['guan-yu'] === undefined &&
|
||||
@@ -1202,7 +1317,7 @@ try {
|
||||
`Expected explicitly loaded corrupted slot timestamp to be normalized: ${JSON.stringify(corruptedSlot)}`
|
||||
);
|
||||
|
||||
console.log('Verified campaign save normalization, reward resettlement, camp reward idempotence, malformed-field/roster-equipment/bond-growth/bond-roster/report-reference/sortie-roster/latest-history/detail recovery, timestamp-safe history/slot recovery, unknown-step/report/history recovery, unsupported-version rejection, and slotted fallback.');
|
||||
console.log('Verified campaign save normalization, reward resettlement, camp reward idempotence, malformed-field/roster-equipment/bond-growth/bond-roster/report-reference/sortie-roster/sortie-preset/latest-history/detail recovery, timestamp-safe history/slot recovery, unknown-step/report/history recovery, unsupported-version rejection, and slotted fallback.');
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user