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

View File

@@ -682,6 +682,48 @@ try {
);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-swap-confirmed.png`, fullPage: true });
const openPresetBookForSavePoint = await readSortiePresetControlPoint(page, 'toggle');
await page.mouse.click(openPresetBookForSavePoint.x, openPresetBookForSavePoint.y);
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortiePresetBrowser?.open === true, undefined, { timeout: 30000 });
const emptyPresetBookState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
emptyPresetBookState?.sortiePresetBrowser?.cards?.length === 3 &&
emptyPresetBookState.sortiePresetBrowser.cards.every((card) => card.saved === false) &&
emptyPresetBookState?.sortieSwapUndo?.available === true,
`Expected three empty preset cards without consuming the pending swap undo: ${JSON.stringify(emptyPresetBookState?.sortiePresetBrowser)}`
);
const saveMobilePresetPoint = await readSortiePresetControlPoint(page, 'save', 'mobile');
await page.mouse.click(saveMobilePresetPoint.x, saveMobilePresetPoint.y);
await page.waitForFunction(() => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.sortieFormationPresets?.mobile?.selectedUnitIds?.length > 0 &&
state?.sortieSwapUndo?.available === true &&
state?.sortieComparisonAction?.kind === 'undo-swap';
}, undefined, { timeout: 30000 });
const savedMobilePresetState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const savedMobilePresetSave = await readCampaignSave(page);
const savedMobilePreset = savedMobilePresetState?.sortieFormationPresets?.mobile;
assert(
JSON.stringify(savedMobilePreset?.selectedUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
savedMobilePreset?.selectedUnitIds?.every((unitId) => Boolean(savedMobilePreset.formationAssignments?.[unitId])) &&
Object.keys(savedMobilePreset ?? {}).sort().join(',') === 'formationAssignments,selectedUnitIds' &&
savedMobilePreset?.sortieItemAssignments === undefined &&
savedMobilePreset?.itemAssignments === undefined,
`Expected mobile preset save to materialize every selected role without storing supplies: ${JSON.stringify(savedMobilePreset)}`
);
assert(
sameJsonValue(savedMobilePresetSave.current?.sortieFormationPresets?.mobile, savedMobilePreset) &&
sameJsonValue(savedMobilePresetSave.slot1?.sortieFormationPresets?.mobile, savedMobilePreset) &&
sameJsonValue(savedMobilePresetState?.sortieItemAssignments, expectedConfirmedItemAssignments) &&
sameJsonValue(savedMobilePresetState?.campaign?.sortieItemAssignments, expectedConfirmedItemAssignments) &&
savedMobilePresetState?.sortieSwapUndo?.available === true,
`Expected preset metadata to sync to current and slot saves without changing the applied roster, roles, supplies, or swap undo: ${JSON.stringify({
state: savedMobilePresetState,
save: savedMobilePresetSave
})}`
);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-preset-saved.png`, fullPage: true });
const undoSwapActionPoint = await readSortieComparisonActionPoint(page);
assert(
undoSwapActionPoint.label === '되돌리기',
@@ -762,6 +804,157 @@ try {
`Expected undo to restore Ma Dai focus and consume the panel action exactly once: ${JSON.stringify(undoneSwapProbe)}`
);
const presetBaselineState = undoneSwapState;
const presetBaselineSave = undoneSwapSave;
const openPresetBookForApplyPoint = await readSortiePresetControlPoint(page, 'toggle');
await page.mouse.click(openPresetBookForApplyPoint.x, openPresetBookForApplyPoint.y);
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortiePresetBrowser?.open === true, undefined, { timeout: 30000 });
const presetBookState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const mobilePresetCard = presetBookState?.sortiePresetBrowser?.cards?.find((card) => card.id === 'mobile');
assert(
presetBookState?.sortiePresetBrowser?.cards?.length === 3 &&
presetBookState?.sortiePresetBrowser?.recommendedPresetId === 'mobile' &&
mobilePresetCard?.saved === true &&
mobilePresetCard?.applicable === true &&
mobilePresetCard?.recommended === true &&
mobilePresetCard?.current === false &&
presetBookState.sortiePresetBrowser.cards.filter((card) => !card.saved).length === 2,
`Expected the only saved valid preset to receive the scenario recommendation badge: ${JSON.stringify(presetBookState?.sortiePresetBrowser)}`
);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-preset-book.png`, fullPage: true });
const hoverMobilePresetPoint = await readSortiePresetControlPoint(page, 'card', 'mobile');
await page.mouse.move(hoverMobilePresetPoint.x, hoverMobilePresetPoint.y);
await page.waitForFunction(() => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.sortiePresetBrowser?.hoveredPresetId === 'mobile' &&
state?.sortieComparisonPreview?.source === 'hover-preset' &&
state?.sortieComparisonPreview?.presetId === 'mobile';
}, undefined, { timeout: 30000 });
const hoverPresetProbe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const view = scene?.sortieComparisonPanelView;
return {
state: window.__HEROS_DEBUG__?.camp(),
panel: {
title: view?.title?.text ?? null,
headline: view?.headline?.text ?? null,
detail: view?.detail?.text ?? null
}
};
});
const hoverPresetState = hoverPresetProbe.state;
const hoverPresetSave = await readCampaignSave(page);
const hoverProjectionCard = hoverPresetState?.sortiePresetBrowser?.cards?.find((card) => card.id === 'mobile');
assert(
hoverPresetState?.sortieComparisonPreview?.mode === 'preset' &&
hoverPresetState?.sortieComparisonPreview?.metrics?.length === 4 &&
hoverPresetState.sortieComparisonPreview.metrics[0]?.label === '공격 합' &&
hoverPresetState.sortieComparisonPreview.metrics[1]?.label === '지력 합' &&
hoverPresetState.sortieComparisonPreview.metrics[2]?.label === '평균 기동' &&
hoverPresetProbe.panel.title === '기동 편성책 비교' &&
hoverPresetProbe.panel.headline?.includes('유지') &&
hoverPresetProbe.panel.detail?.includes('실제 편성 미변경'),
`Expected a whole-formation, non-destructive preset comparison: ${JSON.stringify(hoverPresetProbe)}`
);
assert(
JSON.stringify(hoverPresetState?.selectedSortieUnitIds) === JSON.stringify(presetBaselineState?.selectedSortieUnitIds) &&
sameJsonValue(hoverPresetState?.sortieFormationAssignments, presetBaselineState?.sortieFormationAssignments) &&
sameJsonValue(hoverPresetState?.sortieItemAssignments, presetBaselineState?.sortieItemAssignments) &&
sameJsonValue(hoverPresetSave.current, presetBaselineSave.current) &&
sameJsonValue(hoverPresetSave.slot1, presetBaselineSave.slot1),
`Expected preset hover to preserve runtime, campaign, current save, and slot save: ${JSON.stringify(hoverPresetState)}`
);
const compareMobilePresetPoint = await readSortiePresetControlPoint(page, 'compare', 'mobile');
await page.mouse.click(compareMobilePresetPoint.x, compareMobilePresetPoint.y);
await page.waitForFunction(() => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.sortiePresetBrowser?.pinnedPresetId === 'mobile' &&
state?.sortieComparisonPreview?.source === 'pinned-preset' &&
state?.sortieComparisonAction?.kind === 'confirm-preset';
}, undefined, { timeout: 30000 });
const pinnedPresetState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const pinnedPresetSave = await readCampaignSave(page);
const pinnedProjectionCard = pinnedPresetState?.sortiePresetBrowser?.cards?.find((card) => card.id === 'mobile');
assert(
JSON.stringify(pinnedProjectionCard?.projectedSelectedUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
sameJsonValue(pinnedProjectionCard?.projectedFormationAssignments, savedMobilePreset?.formationAssignments) &&
pinnedPresetState?.sortieComparisonAction?.label === '적용 확정' &&
sameJsonValue(pinnedPresetSave.current, presetBaselineSave.current) &&
sameJsonValue(pinnedPresetSave.slot1, presetBaselineSave.slot1),
`Expected pinning the preset to keep every saved value unchanged until confirmation: ${JSON.stringify(pinnedPresetState)}`
);
const expectedPresetSelectedIds = [...(pinnedProjectionCard?.projectedSelectedUnitIds ?? [])];
const expectedPresetFormationAssignments = { ...(pinnedProjectionCard?.projectedFormationAssignments ?? {}) };
const expectedPresetItemAssignments = structuredClone(pinnedProjectionCard?.projectedItemAssignments ?? {});
const applyPresetActionPoint = await readSortieComparisonActionPoint(page);
assert(applyPresetActionPoint.label === '적용 확정', `Expected preset confirmation label: ${JSON.stringify(applyPresetActionPoint)}`);
await page.mouse.click(applyPresetActionPoint.x, applyPresetActionPoint.y);
await page.waitForFunction((expectedIds) => {
const state = window.__HEROS_DEBUG__?.camp();
return JSON.stringify(state?.selectedSortieUnitIds) === JSON.stringify(expectedIds) &&
state?.sortiePresetUndo?.available === true &&
state?.sortieComparisonAction?.kind === 'undo-preset' &&
state?.sortiePresetBrowser?.open === false;
}, expectedPresetSelectedIds, { timeout: 30000 });
const appliedPresetState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const appliedPresetSave = await readCampaignSave(page);
assert(
JSON.stringify(appliedPresetState?.selectedSortieUnitIds) === JSON.stringify(expectedPresetSelectedIds) &&
JSON.stringify(appliedPresetState?.campaign?.selectedSortieUnitIds) === JSON.stringify(expectedPresetSelectedIds) &&
JSON.stringify(appliedPresetSave.current?.selectedSortieUnitIds) === JSON.stringify(expectedPresetSelectedIds) &&
JSON.stringify(appliedPresetSave.slot1?.selectedSortieUnitIds) === JSON.stringify(expectedPresetSelectedIds) &&
sameJsonValue(appliedPresetState?.sortieFormationAssignments, expectedPresetFormationAssignments) &&
sameJsonValue(appliedPresetState?.campaign?.sortieFormationAssignments, expectedPresetFormationAssignments) &&
sameJsonValue(appliedPresetSave.current?.sortieFormationAssignments, expectedPresetFormationAssignments) &&
sameJsonValue(appliedPresetSave.slot1?.sortieFormationAssignments, expectedPresetFormationAssignments),
`Expected preset confirmation to persist the canonical roster and stored roles everywhere: ${JSON.stringify(appliedPresetState)}`
);
assert(
sameJsonValue(appliedPresetState?.sortieItemAssignments, expectedPresetItemAssignments) &&
sameJsonValue(appliedPresetState?.campaign?.sortieItemAssignments, expectedPresetItemAssignments) &&
sameJsonValue(appliedPresetSave.current?.sortieItemAssignments, expectedPresetItemAssignments) &&
sameJsonValue(appliedPresetSave.slot1?.sortieItemAssignments, expectedPresetItemAssignments) &&
appliedPresetState?.sortieItemAssignments?.['ma-dai'] === undefined &&
appliedPresetState?.sortieItemAssignments?.['guan-yu'] === undefined &&
appliedPresetState?.sortiePresetUndo?.presetId === 'mobile',
`Expected preset apply to preserve only shared officers' supplies without auto-transferring them: ${JSON.stringify({
expected: expectedPresetItemAssignments,
actual: appliedPresetState?.sortieItemAssignments
})}`
);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-preset-applied.png`, fullPage: true });
const undoPresetActionPoint = await readSortieComparisonActionPoint(page);
assert(undoPresetActionPoint.label === '되돌리기', `Expected preset undo label: ${JSON.stringify(undoPresetActionPoint)}`);
await page.mouse.click(undoPresetActionPoint.x, undoPresetActionPoint.y);
await page.waitForFunction((baselineIds) => {
const state = window.__HEROS_DEBUG__?.camp();
return JSON.stringify(state?.selectedSortieUnitIds) === JSON.stringify(baselineIds) &&
state?.sortiePresetUndo === null &&
state?.sortieComparisonAction === null;
}, presetBaselineState?.selectedSortieUnitIds ?? [], { timeout: 30000 });
const undonePresetState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const undonePresetSave = await readCampaignSave(page);
assert(
JSON.stringify(undonePresetState?.selectedSortieUnitIds) === JSON.stringify(presetBaselineState?.selectedSortieUnitIds) &&
sameJsonValue(undonePresetState?.sortieFormationAssignments, presetBaselineState?.sortieFormationAssignments) &&
sameJsonValue(undonePresetState?.sortieItemAssignments, presetBaselineState?.sortieItemAssignments) &&
JSON.stringify(undonePresetSave.current?.selectedSortieUnitIds) === JSON.stringify(presetBaselineSave.current?.selectedSortieUnitIds) &&
JSON.stringify(undonePresetSave.slot1?.selectedSortieUnitIds) === JSON.stringify(presetBaselineSave.slot1?.selectedSortieUnitIds) &&
sameJsonValue(undonePresetSave.current?.sortieFormationAssignments, presetBaselineSave.current?.sortieFormationAssignments) &&
sameJsonValue(undonePresetSave.slot1?.sortieFormationAssignments, presetBaselineSave.slot1?.sortieFormationAssignments) &&
sameJsonValue(undonePresetSave.current?.sortieItemAssignments, presetBaselineSave.current?.sortieItemAssignments) &&
sameJsonValue(undonePresetSave.slot1?.sortieItemAssignments, presetBaselineSave.slot1?.sortieItemAssignments) &&
sameJsonValue(undonePresetSave.current?.sortieFormationPresets?.mobile, savedMobilePreset) &&
sameJsonValue(undonePresetSave.slot1?.sortieFormationPresets?.mobile, savedMobilePreset),
`Expected one-time preset undo to restore the exact roster, roles, and supplies while retaining the saved preset: ${JSON.stringify({
state: undonePresetState,
save: undonePresetSave
})}`
);
const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
await page.mouse.click(658, 196);
await page.waitForFunction((previousScroll) => {
@@ -1280,6 +1473,44 @@ async function readSortieComparisonActionPoint(page) {
return probe;
}
async function readSortiePresetControlPoint(page, control, presetId) {
const probe = await page.evaluate(({ control, presetId }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const state = window.__HEROS_DEBUG__?.camp();
const canvas = document.querySelector('canvas');
const presetCard = state?.sortiePresetBrowser?.cards?.find((card) => card.id === presetId);
const bounds = control === 'toggle'
? state?.sortiePresetBrowser?.toggleButtonBounds
: control === 'close'
? state?.sortiePresetBrowser?.closeButtonBounds
: control === 'card'
? presetCard?.backgroundBounds
: control === 'compare'
? presetCard?.compareButtonBounds
: presetCard?.saveButtonBounds;
if (!scene || !canvas || !bounds) {
return { ready: false, control, presetId: presetId ?? null, bounds: bounds ?? null };
}
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
ready: true,
control,
presetId: presetId ?? null,
bounds,
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
};
}, { control, presetId });
assert(
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
`Expected a visible Phaser preset ${control} control with a canvas-scaled click point: ${JSON.stringify(probe)}`
);
return probe;
}
async function readBattleDoctrineProbe(page) {
return page.evaluate(() => {
const state = window.__HEROS_DEBUG__?.battle();