fix: resume campaign progress safely
This commit is contained in:
@@ -206,18 +206,43 @@ try {
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-new-game-story.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'new game story');
|
||||
await assertFreshStoryAssetStreaming(page, requestedResourceUrls, freshStoryRequestStartIndex);
|
||||
const interruptedPrologue = await page.evaluate(
|
||||
() => window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.()
|
||||
);
|
||||
const interruptedPrologueSave = await readCampaignSave(page);
|
||||
assert(
|
||||
Number.isInteger(interruptedPrologue?.pageIndex) &&
|
||||
interruptedPrologue.pageIndex > 0 &&
|
||||
Boolean(interruptedPrologue.currentPageId),
|
||||
`Expected the prologue interruption fixture to be beyond its first page: ${JSON.stringify(interruptedPrologue)}`
|
||||
);
|
||||
assertStoryCheckpointMatchesPage(
|
||||
interruptedPrologue,
|
||||
interruptedPrologueSave,
|
||||
'interrupted prologue story'
|
||||
);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await waitForTitle(page);
|
||||
await clickLegacyUi(page, 962, 310);
|
||||
await waitForStoryReady(page);
|
||||
const resumedPrologue = await page.evaluate(() => window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.());
|
||||
const resumedPrologueSave = await readCampaignSave(page);
|
||||
assert(
|
||||
resumedPrologue?.pageIndex === 0 &&
|
||||
resumedPrologue?.pageIndex === interruptedPrologue.pageIndex &&
|
||||
resumedPrologue?.currentPageId === interruptedPrologue.currentPageId &&
|
||||
resumedPrologue?.presentation?.battleId === 'first-battle-zhuo-commandery' &&
|
||||
resumedPrologue.presentation.arcId === 'yellow-turban' &&
|
||||
resumedPrologue.presentation.stage === 'story' &&
|
||||
resumedPrologue.presentation.washVisible === true,
|
||||
`Expected a resumed prologue to retain the founding campaign presentation: ${JSON.stringify(resumedPrologue)}`
|
||||
`Expected a resumed prologue to retain the exact interrupted page and founding campaign presentation: ${JSON.stringify({
|
||||
interrupted: interruptedPrologue,
|
||||
resumed: resumedPrologue
|
||||
})}`
|
||||
);
|
||||
assertStoryCheckpointMatchesPage(
|
||||
resumedPrologue,
|
||||
resumedPrologueSave,
|
||||
'resumed prologue story'
|
||||
);
|
||||
const firstBattleTransition = await advanceUntilBattle(page, 'first-battle-zhuo-commandery', {
|
||||
startDeployment: false,
|
||||
@@ -2651,6 +2676,20 @@ try {
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const interruptedFirstAftermath = await page.evaluate(
|
||||
() => window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.()
|
||||
);
|
||||
const interruptedFirstAftermathSave = await readCampaignSave(page);
|
||||
assert(
|
||||
interruptedFirstAftermath?.pageIndex === 1 &&
|
||||
Boolean(interruptedFirstAftermath.currentPageId),
|
||||
`Expected the first aftermath interruption fixture on its second page: ${JSON.stringify(interruptedFirstAftermath)}`
|
||||
);
|
||||
assertStoryCheckpointMatchesPage(
|
||||
interruptedFirstAftermath,
|
||||
interruptedFirstAftermathSave,
|
||||
'interrupted first aftermath'
|
||||
);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await waitForTitle(page);
|
||||
await clickLegacyUi(page, 962, 310);
|
||||
@@ -2658,13 +2697,23 @@ try {
|
||||
const resumedFirstAftermath = await page.evaluate(() => window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.());
|
||||
const resumedFirstAftermathSave = await readCampaignSave(page);
|
||||
assert(
|
||||
resumedFirstAftermath?.pageIndex === 0 &&
|
||||
resumedFirstAftermath?.pageIndex === interruptedFirstAftermath.pageIndex &&
|
||||
resumedFirstAftermath?.currentPageId === interruptedFirstAftermath.currentPageId &&
|
||||
resumedFirstAftermath?.totalPages === 5 &&
|
||||
resumedFirstAftermath?.presentation?.battleId === 'first-battle-zhuo-commandery' &&
|
||||
resumedFirstAftermath.presentation.stage === 'aftermath' &&
|
||||
resumedFirstAftermathSave.current?.pendingAftermathBattleId === 'first-battle-zhuo-commandery' &&
|
||||
resumedFirstAftermathSave.slot1?.pendingAftermathBattleId === 'first-battle-zhuo-commandery',
|
||||
`Expected continue after an interrupted aftermath to restore that aftermath before camp: ${JSON.stringify({ story: resumedFirstAftermath, save: resumedFirstAftermathSave })}`
|
||||
`Expected continue after an interrupted aftermath to restore the exact interrupted page before camp: ${JSON.stringify({
|
||||
interrupted: interruptedFirstAftermath,
|
||||
resumed: resumedFirstAftermath,
|
||||
save: resumedFirstAftermathSave
|
||||
})}`
|
||||
);
|
||||
assertStoryCheckpointMatchesPage(
|
||||
resumedFirstAftermath,
|
||||
resumedFirstAftermathSave,
|
||||
'resumed first aftermath'
|
||||
);
|
||||
assert(
|
||||
sameJsonValue(
|
||||
@@ -2704,19 +2753,23 @@ try {
|
||||
'first-victory-zhang-fei',
|
||||
'first-victory-next'
|
||||
];
|
||||
const firstAftermathRewardDisplays = [
|
||||
directFirstAftermath.rewardDisplay,
|
||||
...firstCampTransition.rewardDisplays
|
||||
];
|
||||
assert(
|
||||
sameJsonValue(
|
||||
firstCampTransition.rewardDisplays.map((display) => display.pageId),
|
||||
firstAftermathRewardDisplays.map((display) => display.pageId),
|
||||
firstVictoryStoryPageIds
|
||||
) &&
|
||||
firstCampTransition.rewardDisplays.every((display) => (
|
||||
firstAftermathRewardDisplays.every((display) => (
|
||||
display.source === 'campaign' &&
|
||||
display.battleId === 'first-battle-zhuo-commandery' &&
|
||||
display.rewardGold === null &&
|
||||
Array.isArray(display.items) &&
|
||||
display.items.length === 0
|
||||
)),
|
||||
`Expected all five first-victory aftermath pages to retain campaign context without duplicating exact rewards: ${JSON.stringify(firstCampTransition.rewardDisplays)}`
|
||||
`Expected all five first-victory aftermath pages to retain campaign context without duplicating exact rewards: ${JSON.stringify(firstAftermathRewardDisplays)}`
|
||||
);
|
||||
const firstNarrativeMemoriesByPage = Object.fromEntries(
|
||||
firstCampTransition.narrativeMemories.map((memory) => [memory.pageId, memory])
|
||||
@@ -6131,6 +6184,15 @@ try {
|
||||
await waitForTitle(page);
|
||||
}
|
||||
|
||||
const midCampaignSelectedUnitIds = [
|
||||
'zhuge-liang',
|
||||
'jiang-wei',
|
||||
'wang-ping',
|
||||
'zhao-yun',
|
||||
'huang-quan',
|
||||
'ma-liang',
|
||||
'ma-dai'
|
||||
];
|
||||
await seedCampaignSave(page, {
|
||||
battleId: 'fifty-eighth-battle-qishan-retreat',
|
||||
battleTitle: '기산 후퇴로',
|
||||
@@ -6138,12 +6200,36 @@ try {
|
||||
gold: 7400,
|
||||
turnNumber: 13,
|
||||
defeatedEnemies: 16,
|
||||
selectedSortieUnitIds: ['zhuge-liang', 'jiang-wei', 'wang-ping', 'zhao-yun', 'huang-quan', 'ma-liang', 'ma-dai']
|
||||
selectedSortieUnitIds: midCampaignSelectedUnitIds
|
||||
});
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await waitForTitle(page);
|
||||
await clickLegacyUi(page, 962, 310);
|
||||
await waitForCamp(page);
|
||||
const midCampaignSelectionFixture = await page.evaluate((requestedUnitIds) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
if (
|
||||
!scene ||
|
||||
typeof scene.normalizedSortieUnitIds !== 'function' ||
|
||||
typeof scene.persistSortieSelection !== 'function'
|
||||
) {
|
||||
return { ready: false, selectedUnitIds: [] };
|
||||
}
|
||||
scene.selectedSortieUnitIds = scene.normalizedSortieUnitIds(requestedUnitIds);
|
||||
scene.persistSortieSelection();
|
||||
return {
|
||||
ready: true,
|
||||
selectedUnitIds: [...scene.selectedSortieUnitIds]
|
||||
};
|
||||
}, midCampaignSelectedUnitIds);
|
||||
assert(
|
||||
midCampaignSelectionFixture.ready === true &&
|
||||
sameJsonValue(
|
||||
[...midCampaignSelectionFixture.selectedUnitIds].sort(),
|
||||
[...midCampaignSelectedUnitIds].sort()
|
||||
),
|
||||
`Expected the mid-campaign fixture to select all seven officers after camp recruitment: ${JSON.stringify(midCampaignSelectionFixture)}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-campaign-continue.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'mid campaign camp');
|
||||
|
||||
@@ -6597,8 +6683,12 @@ try {
|
||||
JSON.stringify(swapPreview?.comparison?.projected?.selectedUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
|
||||
swapPreview?.metrics?.length === 4 &&
|
||||
swapPreview.metrics.some((metric) => metric.delta !== 0) &&
|
||||
swapPreview?.comparison?.lostRoles?.includes('flank') &&
|
||||
swapPreview?.comparison?.activeBondDelta === 0 &&
|
||||
swapPreview?.comparison?.current?.coveredRoleCount === 3 &&
|
||||
swapPreview?.comparison?.projected?.coveredRoleCount === 3 &&
|
||||
swapPreview?.comparison?.current?.formationRoleCounts?.flank === 2 &&
|
||||
swapPreview?.comparison?.projected?.formationRoleCounts?.flank === 1 &&
|
||||
swapPreview?.comparison?.lostRoles?.length === 0 &&
|
||||
swapPreview?.comparison?.activeBondDelta === -1 &&
|
||||
swapPreview?.comparison?.gainedBonds?.length > 0 &&
|
||||
swapPreview?.comparison?.lostBonds?.length > 0 &&
|
||||
swapPreview?.strategyCoverage?.current?.count === 4 &&
|
||||
@@ -6617,7 +6707,7 @@ try {
|
||||
midFormationSwapPreviewProbe.panel.headline?.includes('IN 관우') &&
|
||||
midFormationSwapPreviewProbe.panel.impact?.includes('전법 4/4→4/4') &&
|
||||
midFormationSwapPreviewProbe.panel.impact?.includes('추격') &&
|
||||
midFormationSwapPreviewProbe.panel.impact?.includes('공백 돌파') &&
|
||||
midFormationSwapPreviewProbe.panel.impact?.includes('역할 유지') &&
|
||||
midFormationSwapPreviewProbe.panel.detail?.includes('전법 유지') &&
|
||||
midFormationSwapPreviewProbe.panel.detail?.includes('추격 해제') &&
|
||||
midFormationSwapPreviewProbe.panel.detail?.includes('8%') &&
|
||||
@@ -7753,6 +7843,61 @@ try {
|
||||
campaignSaveWithoutCampRecruitmentEffects(updatedMobileResultSave)
|
||||
)
|
||||
);
|
||||
const midImprovementStoryCheckpointConsumed = ['current', 'slot1'].every(
|
||||
(slot) => {
|
||||
const before = updatedMobileResultSave[slot];
|
||||
const after = pendingMidImprovementSave[slot];
|
||||
const checkpoint = before?.storyCheckpoint;
|
||||
return (
|
||||
checkpoint?.phase === 'reading' &&
|
||||
checkpoint.pageIndex === 0 &&
|
||||
checkpoint.campaignStep === before?.step &&
|
||||
checkpoint.revision ===
|
||||
before?.storyCheckpointRevision &&
|
||||
checkpoint.continuationIntent === 'default' &&
|
||||
typeof checkpoint.sequenceId === 'string' &&
|
||||
checkpoint.sequenceId.length > 0 &&
|
||||
after?.storyCheckpoint === undefined &&
|
||||
after?.storyCheckpointRevision ===
|
||||
expectedNextStoryCheckpointRevision(
|
||||
before?.storyCheckpointRevision
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
const expectedMidImprovementTargetStep =
|
||||
typeof midImprovementTargetBattleId === 'string'
|
||||
? midImprovementTargetBattleId.replace(
|
||||
/-battle-.+$/,
|
||||
'-battle'
|
||||
)
|
||||
: undefined;
|
||||
const midImprovementPreparationCheckpointPersisted = [
|
||||
'current',
|
||||
'slot1'
|
||||
].every((slot) => {
|
||||
const before = updatedMobileResultSave[slot];
|
||||
const after = pendingMidImprovementSave[slot];
|
||||
const checkpoint =
|
||||
after?.sortiePreparationCheckpoint;
|
||||
return (
|
||||
before?.sortiePreparationCheckpoint ===
|
||||
undefined &&
|
||||
checkpoint?.version === 1 &&
|
||||
checkpoint.intent ===
|
||||
'sortie-improvement' &&
|
||||
checkpoint.mode === 'next-sortie' &&
|
||||
checkpoint.sourceBattleId ===
|
||||
midCampNextBattleId &&
|
||||
checkpoint.targetBattleId ===
|
||||
midImprovementTargetBattleId &&
|
||||
checkpoint.targetCampaignStep ===
|
||||
expectedMidImprovementTargetStep &&
|
||||
Number.isFinite(
|
||||
Date.parse(checkpoint.savedAt)
|
||||
)
|
||||
);
|
||||
});
|
||||
const midImprovementAftermathCompleted =
|
||||
updatedMobileResultSave.current?.pendingAftermathBattleId === midCampNextBattleId &&
|
||||
updatedMobileResultSave.slot1?.pendingAftermathBattleId === midCampNextBattleId &&
|
||||
@@ -7802,19 +7947,63 @@ try {
|
||||
boundsWithinFhdViewport(midImprovementLedger.toggleButtonBounds) &&
|
||||
midImprovementLedger.toggleInteractive === true &&
|
||||
midImprovementLedger.cards.length === 0 &&
|
||||
midImprovementLedger.actions.length === 0 &&
|
||||
midImprovementCampEntrySaveUnchanged &&
|
||||
midImprovementAftermathCompleted &&
|
||||
midImprovementRewardDestinationPending,
|
||||
midImprovementLedger.actions.length === 0 &&
|
||||
midImprovementCampEntrySaveUnchanged &&
|
||||
midImprovementStoryCheckpointConsumed &&
|
||||
midImprovementPreparationCheckpointPersisted &&
|
||||
midImprovementAftermathCompleted &&
|
||||
midImprovementRewardDestinationPending,
|
||||
`Expected next-sortie improvement arrival to remain nonblocking with passive settlement and an unopened on-demand ledger: ${JSON.stringify({
|
||||
openSortiePrepOnCreate: pendingMidImprovementReward?.openSortiePrepOnCreate,
|
||||
sortieVisible: pendingMidImprovementReward?.sortieVisible,
|
||||
arrival: midImprovementArrival,
|
||||
ledger: midImprovementLedger,
|
||||
expectedBattleId: midCampNextBattleId,
|
||||
saveUnchanged: midImprovementCampEntrySaveUnchanged,
|
||||
aftermathCompleted: midImprovementAftermathCompleted,
|
||||
rewardDestinationPending: midImprovementRewardDestinationPending,
|
||||
expectedBattleId: midCampNextBattleId,
|
||||
saveUnchanged: midImprovementCampEntrySaveUnchanged,
|
||||
storyCheckpointConsumed:
|
||||
midImprovementStoryCheckpointConsumed,
|
||||
storyCheckpointBefore: {
|
||||
current:
|
||||
updatedMobileResultSave.current
|
||||
?.storyCheckpoint,
|
||||
slot1:
|
||||
updatedMobileResultSave.slot1
|
||||
?.storyCheckpoint
|
||||
},
|
||||
storyCheckpointAfter: {
|
||||
current:
|
||||
pendingMidImprovementSave.current
|
||||
?.storyCheckpoint,
|
||||
slot1:
|
||||
pendingMidImprovementSave.slot1
|
||||
?.storyCheckpoint
|
||||
},
|
||||
storyCheckpointRevisions: {
|
||||
beforeCurrent:
|
||||
updatedMobileResultSave.current
|
||||
?.storyCheckpointRevision,
|
||||
beforeSlot1:
|
||||
updatedMobileResultSave.slot1
|
||||
?.storyCheckpointRevision,
|
||||
afterCurrent:
|
||||
pendingMidImprovementSave.current
|
||||
?.storyCheckpointRevision,
|
||||
afterSlot1:
|
||||
pendingMidImprovementSave.slot1
|
||||
?.storyCheckpointRevision
|
||||
},
|
||||
preparationCheckpointPersisted:
|
||||
midImprovementPreparationCheckpointPersisted,
|
||||
preparationCheckpoint: {
|
||||
current:
|
||||
pendingMidImprovementSave.current
|
||||
?.sortiePreparationCheckpoint,
|
||||
slot1:
|
||||
pendingMidImprovementSave.slot1
|
||||
?.sortiePreparationCheckpoint
|
||||
},
|
||||
aftermathCompleted: midImprovementAftermathCompleted,
|
||||
rewardDestinationPending: midImprovementRewardDestinationPending,
|
||||
rewardAcknowledgements: midImprovementRewardAcknowledgements,
|
||||
noticeDismissals: midImprovementNoticeDismissals
|
||||
})}`
|
||||
@@ -11375,6 +11564,39 @@ async function readCampaignSave(page) {
|
||||
});
|
||||
}
|
||||
|
||||
function assertStoryCheckpointMatchesPage(story, saves, label) {
|
||||
const runtimeCheckpoint =
|
||||
story?.storyCheckpoint?.persisted;
|
||||
const currentCheckpoint = saves?.current?.storyCheckpoint;
|
||||
const slotCheckpoint = saves?.slot1?.storyCheckpoint;
|
||||
assert(
|
||||
story?.storyCheckpoint?.enabled === true &&
|
||||
Boolean(story.currentPageId) &&
|
||||
runtimeCheckpoint?.sequenceId ===
|
||||
story.storyCheckpoint.sequenceId &&
|
||||
runtimeCheckpoint?.beatId === story.currentPageId &&
|
||||
runtimeCheckpoint?.pageIndex === story.pageIndex &&
|
||||
runtimeCheckpoint?.phase === 'reading' &&
|
||||
currentCheckpoint?.sequenceId ===
|
||||
runtimeCheckpoint.sequenceId &&
|
||||
currentCheckpoint?.beatId === story.currentPageId &&
|
||||
currentCheckpoint?.pageIndex === story.pageIndex &&
|
||||
currentCheckpoint?.phase === 'reading' &&
|
||||
slotCheckpoint?.sequenceId ===
|
||||
runtimeCheckpoint.sequenceId &&
|
||||
slotCheckpoint?.beatId === story.currentPageId &&
|
||||
slotCheckpoint?.pageIndex === story.pageIndex &&
|
||||
slotCheckpoint?.phase === 'reading' &&
|
||||
sameJsonValue(currentCheckpoint, slotCheckpoint),
|
||||
`Expected ${label} to persist its exact page checkpoint in runtime, current, and slot saves: ${JSON.stringify({
|
||||
story,
|
||||
runtimeCheckpoint,
|
||||
currentCheckpoint,
|
||||
slotCheckpoint
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
async function readBattleResultEvaluationControlPoint(page, control, presetId) {
|
||||
const probe = await page.evaluate(({ control, presetId }) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
@@ -12558,6 +12780,16 @@ function sameJsonValue(value, expected) {
|
||||
return stableJson(value) === stableJson(expected);
|
||||
}
|
||||
|
||||
function expectedNextStoryCheckpointRevision(revision) {
|
||||
const normalized =
|
||||
Number.isSafeInteger(revision) && revision >= 0
|
||||
? revision
|
||||
: 0;
|
||||
return normalized >= Number.MAX_SAFE_INTEGER
|
||||
? 0
|
||||
: normalized + 1;
|
||||
}
|
||||
|
||||
function assertBattleResultDebriefText(resultTexts, rewards, context) {
|
||||
const texts = Array.isArray(resultTexts) ? resultTexts : [];
|
||||
const recruits = rewards?.recruits ?? [];
|
||||
@@ -12604,6 +12836,10 @@ function campaignSaveWithoutCampRecruitmentEffects(save) {
|
||||
acknowledgedVictoryRewardBattleIds: _acknowledgedVictoryRewardBattleIds,
|
||||
acknowledgedVictoryRewardCategories: _acknowledgedVictoryRewardCategories,
|
||||
pendingAftermathBattleId: _pendingAftermathBattleId,
|
||||
storyCheckpointRevision: _storyCheckpointRevision,
|
||||
storyCheckpoint: _storyCheckpoint,
|
||||
sortiePreparationCheckpoint:
|
||||
_sortiePreparationCheckpoint,
|
||||
firstBattleReport,
|
||||
...stableFields
|
||||
} = state;
|
||||
|
||||
Reference in New Issue
Block a user