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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user