feat: add sortie formation presets

This commit is contained in:
2026-07-10 21:26:54 +09:00
parent 481e870321
commit 7ba51d9243
4 changed files with 1174 additions and 36 deletions

View File

@@ -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();
}