diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index ec884cf..193f57a 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -78,10 +78,20 @@ try { const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? []; return activeScenes.some((scene) => scene.scene.key === 'BattleScene'); }, undefined, { timeout: 90000 }); - await page.waitForFunction(() => { - const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.phase === 'idle' && state?.mapBackgroundReady === true; - }, undefined, { timeout: 90000 }); + try { + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true; + }, undefined, { timeout: 90000 }); + } catch (error) { + const battleReadiness = await page.evaluate(() => ({ + activeScenes: window.__HEROS_DEBUG__?.activeScenes?.() ?? [], + battle: window.__HEROS_DEBUG__?.battle?.() ?? null + })); + throw new Error(`First battle did not become ready: ${JSON.stringify(battleReadiness)}`, { cause: error }); + } + + await startDeploymentIfNeeded(page, 'first-battle-zhuo-commandery'); await page.keyboard.press('F9'); await page.waitForTimeout(100); @@ -326,14 +336,23 @@ try { scene.clearMarkers(); scene.hideCommandMenu(); scene.hideTurnEndPrompt(); + scene.hideBattleEventBanner(); }); await page.mouse.click(260, 300, { button: 'right' }); await page.waitForTimeout(120); - await page.mouse.click(182, 230); + await page.mouse.click(182, 212); await page.waitForTimeout(120); await page.mouse.click(400, 213); - await page.waitForTimeout(180); + await page.waitForFunction(() => ( + window.localStorage.getItem('heros-web:battle:first-battle-zhuo-commandery:slot-1') !== null || + window.__HEROS_DEBUG__?.battle()?.turnPromptMode === 'save-overwrite-confirm' + )); + const needsBattleSaveOverwrite = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.turnPromptMode === 'save-overwrite-confirm'); + if (needsBattleSaveOverwrite) { + await page.keyboard.press('Enter'); + } + await page.waitForFunction(() => window.localStorage.getItem('heros-web:battle:first-battle-zhuo-commandery:slot-1') !== null); const battleSlotSave = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:battle:first-battle-zhuo-commandery:slot-1'); @@ -345,10 +364,15 @@ try { await page.mouse.click(260, 300, { button: 'right' }); await page.waitForTimeout(120); - await page.mouse.click(182, 264); + await page.mouse.click(182, 246); await page.waitForTimeout(120); await page.mouse.click(400, 213); - await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.scene === 'BattleScene'); + await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.turnPromptMode === 'load-confirm'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.turnPromptVisible === false && state?.saveSlotPanelVisible === false; + }); const cameraBeforeScroll = result.battleState.camera; if ( @@ -404,7 +428,7 @@ try { await page.screenshot({ path: 'dist/verification-battle-result-victory.png', fullPage: true }); await page.mouse.click(738, 642); - await waitForCampReady(page); + await waitForCampAfterBattleResult(page); await page.screenshot({ path: 'dist/verification-camp.png', fullPage: true }); const campState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); @@ -433,7 +457,7 @@ try { }); campScene.render(); }); - await page.mouse.click(888, 38); + await page.mouse.click(798, 38); await page.waitForTimeout(160); await page.screenshot({ path: 'dist/verification-camp-supplies.png', fullPage: true }); await page.mouse.click(1120, 245); @@ -460,7 +484,7 @@ try { throw new Error(`Expected merchant purchase to add bean and spend gold: ${JSON.stringify({ goldBeforeMerchant, campStateAfterMerchant })}`); } - await page.mouse.click(732, 38); + await page.mouse.click(650, 38); await page.waitForTimeout(120); await page.mouse.click(974, 482); await page.waitForTimeout(260); @@ -502,8 +526,8 @@ try { assertSortieTacticalRoster(campStateWithSortiePrep, ['liu-bei', 'guan-yu', 'zhang-fei']); await page.screenshot({ path: 'dist/verification-camp-sortie.png', fullPage: true }); - await page.mouse.click(922, 646); - await page.waitForTimeout(140); + await page.keyboard.press('Escape'); + await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortieVisible === false); const campStateAfterSortieClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (campStateAfterSortieClose?.sortieVisible) { throw new Error(`Expected sortie preparation overlay to close: ${JSON.stringify(campStateAfterSortieClose)}`); @@ -511,7 +535,7 @@ try { await page.mouse.click(1120, 38); await page.waitForTimeout(120); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 24; i += 1) { @@ -530,8 +554,9 @@ try { await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'second-battle-yellow-turban-pursuit' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'second-battle-yellow-turban-pursuit' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'second-battle-yellow-turban-pursuit'); await page.screenshot({ path: 'dist/verification-second-battle.png', fullPage: true }); const secondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -580,7 +605,7 @@ try { if (!secondCampSortieState?.sortieVisible) { throw new Error(`Expected second camp sortie preparation overlay: ${JSON.stringify(secondCampSortieState)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 24; i += 1) { @@ -599,8 +624,9 @@ try { await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'third-battle-guangzong-road'); await page.screenshot({ path: 'dist/verification-third-battle.png', fullPage: true }); const thirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -646,7 +672,7 @@ try { if (!thirdCampSortieState?.sortieVisible) { throw new Error(`Expected third camp sortie preparation overlay: ${JSON.stringify(thirdCampSortieState)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 26; i += 1) { @@ -665,8 +691,9 @@ try { await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fourth-battle-guangzong-camp' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fourth-battle-guangzong-camp' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fourth-battle-guangzong-camp'); await page.screenshot({ path: 'dist/verification-fourth-battle.png', fullPage: true }); const fourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -712,7 +739,7 @@ try { if (!fourthCampSortieState?.sortieVisible) { throw new Error(`Expected fourth camp sortie preparation overlay: ${JSON.stringify(fourthCampSortieState)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 26; i += 1) { @@ -731,8 +758,9 @@ try { await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifth-battle-sishui-vanguard' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifth-battle-sishui-vanguard' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifth-battle-sishui-vanguard'); await page.screenshot({ path: 'dist/verification-fifth-battle.png', fullPage: true }); const fifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -788,7 +816,7 @@ try { if (!fifthCampSortieState?.sortieVisible) { throw new Error(`Expected fifth camp sortie preparation overlay: ${JSON.stringify(fifthCampSortieState)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 26; i += 1) { @@ -807,8 +835,9 @@ try { await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixth-battle-jieqiao-relief' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixth-battle-jieqiao-relief' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixth-battle-jieqiao-relief'); await page.screenshot({ path: 'dist/verification-sixth-battle.png', fullPage: true }); const sixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -894,7 +923,7 @@ try { ) { throw new Error(`Expected benching Guan Yu to leave one bond and preview restoring two: ${JSON.stringify(sixthCampAfterSortieToggle.sortieFocusedSynergyPreview)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-seventh-bridge-story.png', fullPage: true }); @@ -913,8 +942,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'seventh-battle-xuzhou-rescue' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'seventh-battle-xuzhou-rescue' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'seventh-battle-xuzhou-rescue'); await page.screenshot({ path: 'dist/verification-seventh-battle.png', fullPage: true }); const seventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -985,7 +1015,7 @@ try { throw new Error(`Expected recruit to be selectable in seventh camp sortie prep: ${JSON.stringify(seventhCampRecruitSortieState)}`); } assertSortieTacticalRoster(seventhCampRecruitSortieState, ['liu-bei', 'zhang-fei', 'jian-yong', 'mi-zhu']); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xuzhou-entrust-story.png', fullPage: true }); @@ -1002,8 +1032,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'eighth-battle-xiaopei-supply-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'eighth-battle-xiaopei-supply-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'eighth-battle-xiaopei-supply-road'); await page.screenshot({ path: 'dist/verification-eighth-battle.png', fullPage: true }); const eighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1062,7 +1093,7 @@ try { throw new Error(`Expected Mi Zhu to be selectable for ninth battle sortie: ${JSON.stringify(eighthCampMiZhuSortieState)}`); } assertSortieTacticalRoster(eighthCampMiZhuSortieState, ['liu-bei', 'zhang-fei', 'jian-yong', 'mi-zhu']); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-lubu-refuge-story.png', fullPage: true }); @@ -1079,8 +1110,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'ninth-battle-xuzhou-gate-night-raid' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'ninth-battle-xuzhou-gate-night-raid' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'ninth-battle-xuzhou-gate-night-raid'); await page.screenshot({ path: 'dist/verification-ninth-battle.png', fullPage: true }); const ninthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1140,7 +1172,7 @@ try { throw new Error(`Expected Guan Yu to be selectable again for Xuzhou breakout: ${JSON.stringify(ninthCampGuanYuSortieState)}`); } assertSortieTacticalRoster(ninthCampGuanYuSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xuzhou-loss-story.png', fullPage: true }); @@ -1157,8 +1189,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'tenth-battle-xuzhou-breakout' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'tenth-battle-xuzhou-breakout' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'tenth-battle-xuzhou-breakout'); await page.screenshot({ path: 'dist/verification-tenth-battle.png', fullPage: true }); const tenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1211,7 +1244,7 @@ try { throw new Error(`Expected tenth camp sortie prep overlay: ${JSON.stringify(tenthCampSortieState)}`); } assertSortieTacticalRoster(tenthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-cao-cao-refuge-story.png', fullPage: true }); @@ -1228,8 +1261,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'eleventh-battle-xudu-refuge-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'eleventh-battle-xudu-refuge-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'eleventh-battle-xudu-refuge-road'); await page.screenshot({ path: 'dist/verification-eleventh-battle.png', fullPage: true }); const eleventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1282,7 +1316,7 @@ try { throw new Error(`Expected eleventh camp sortie prep to target Xiapi outer siege: ${JSON.stringify(eleventhCampSortieState)}`); } assertSortieTacticalRoster(eleventhCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xiapi-siege-story.png', fullPage: true }); @@ -1299,8 +1333,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twelfth-battle-xiapi-outer-siege' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twelfth-battle-xiapi-outer-siege' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twelfth-battle-xiapi-outer-siege'); await page.screenshot({ path: 'dist/verification-twelfth-battle.png', fullPage: true }); const twelfthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1362,7 +1397,7 @@ try { if (!twelfthCampSortieState.sortieRoster.some((unit) => unit.id === 'jian-yong' && unit.recruited)) { throw new Error(`Expected recruit officers to be marked in sortie roster: ${JSON.stringify(twelfthCampSortieState.sortieRoster)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-xiapi-final-story.png', fullPage: true }); @@ -1379,8 +1414,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirteenth-battle-xiapi-final' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirteenth-battle-xiapi-final' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirteenth-battle-xiapi-final'); await page.screenshot({ path: 'dist/verification-thirteenth-battle.png', fullPage: true }); const thirteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1447,7 +1483,7 @@ try { } await page.screenshot({ path: 'dist/verification-cao-break-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-cao-break-story.png', fullPage: true }); @@ -1464,8 +1500,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fourteenth-battle-cao-break' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fourteenth-battle-cao-break' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fourteenth-battle-cao-break'); await page.screenshot({ path: 'dist/verification-fourteenth-battle.png', fullPage: true }); const fourteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1519,7 +1556,7 @@ try { assertSortieTacticalRoster(fourteenthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian']); await page.screenshot({ path: 'dist/verification-yuan-refuge-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yuan-refuge-story.png', fullPage: true }); @@ -1536,8 +1573,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifteenth-battle-yuan-refuge-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifteenth-battle-yuan-refuge-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifteenth-battle-yuan-refuge-road'); await page.screenshot({ path: 'dist/verification-fifteenth-battle.png', fullPage: true }); const fifteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1617,7 +1655,7 @@ try { } await page.screenshot({ path: 'dist/verification-liu-biao-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-liu-biao-story.png', fullPage: true }); @@ -1634,8 +1672,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixteenth-battle-liu-biao-refuge' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixteenth-battle-liu-biao-refuge' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixteenth-battle-liu-biao-refuge'); await page.screenshot({ path: 'dist/verification-sixteenth-battle.png', fullPage: true }); const sixteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1678,7 +1717,7 @@ try { throw new Error(`Expected sixteenth camp to expose Liu Biao refuge dialogue/visit sets and preserve Zhao Yun in roster: ${JSON.stringify(sixteenthCampState)}`); } - await page.mouse.click(810, 38); + await page.mouse.click(724, 38); await page.waitForTimeout(160); const sixteenthVisitTabState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( @@ -1725,7 +1764,7 @@ try { ]); await page.screenshot({ path: 'dist/verification-wolong-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-wolong-story.png', fullPage: true }); @@ -1742,8 +1781,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'seventeenth-battle-wolong-visit-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'seventeenth-battle-wolong-visit-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'seventeenth-battle-wolong-visit-road'); await page.screenshot({ path: 'dist/verification-seventeenth-battle.png', fullPage: true }); const seventeenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1845,7 +1885,7 @@ try { } await page.screenshot({ path: 'dist/verification-bowang-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-bowang-story.png', fullPage: true }); @@ -1862,8 +1902,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'eighteenth-battle-bowang-ambush' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'eighteenth-battle-bowang-ambush' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'eighteenth-battle-bowang-ambush'); await page.screenshot({ path: 'dist/verification-eighteenth-battle.png', fullPage: true }); const eighteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -1913,13 +1954,13 @@ try { eighteenthCampState.rosterCollection?.reserveTrainingExp < 12 || !eighteenthCampState.reserveTrainingAwards?.some((entry) => entry.unitId === 'jian-yong' && entry.expGained === 6) || !eighteenthCampState.reserveTrainingAwards?.every((entry) => entry.expGained === 6 && entry.equipmentExpGained === 2) || - !eighteenthCampState.reserveTrainingAwards?.some((entry) => ['mi-zhu', 'sun-qian'].includes(entry.unitId)) + !eighteenthCampState.reserveTrainingAwards?.some((entry) => ['mi-zhu', 'sun-qian', 'zhao-yun'].includes(entry.unitId)) ) { throw new Error(`Expected eighteenth camp to expose Bowang dialogue/visit sets, Zhuge bonds, and reserve training growth: ${JSON.stringify(eighteenthCampState)}`); } await page.screenshot({ path: 'dist/verification-bowang-camp.png', fullPage: true }); - await page.mouse.click(654, 38); + await page.mouse.click(576, 38); await page.waitForTimeout(160); await page.mouse.click(180, 315); await page.waitForTimeout(160); @@ -2000,11 +2041,11 @@ try { } await page.screenshot({ path: 'dist/verification-changban-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-changban-story.png', fullPage: true }); - for (let i = 0; i < 30; i += 1) { + for (let i = 0; i < 50; i += 1) { const enteredNineteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'nineteenth-battle-changban-refuge'; @@ -2017,8 +2058,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'nineteenth-battle-changban-refuge' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; - }); + return state?.scene === 'BattleScene' && state?.battleId === 'nineteenth-battle-changban-refuge' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; + }, undefined, { timeout: 90000 }); + await startDeploymentIfNeeded(page, 'nineteenth-battle-changban-refuge'); await page.screenshot({ path: 'dist/verification-nineteenth-battle.png', fullPage: true }); const nineteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2132,7 +2174,7 @@ try { } await page.screenshot({ path: 'dist/verification-jiangdong-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jiangdong-story.png', fullPage: true }); @@ -2149,8 +2191,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twentieth-battle-jiangdong-envoy' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twentieth-battle-jiangdong-envoy' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twentieth-battle-jiangdong-envoy'); await page.screenshot({ path: 'dist/verification-twentieth-battle.png', fullPage: true }); const twentiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2265,7 +2308,7 @@ try { } await page.screenshot({ path: 'dist/verification-redcliffs-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-redcliffs-story.png', fullPage: true }); @@ -2282,8 +2325,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-first-battle-red-cliffs-vanguard' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-first-battle-red-cliffs-vanguard' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-first-battle-red-cliffs-vanguard'); await page.screenshot({ path: 'dist/verification-twenty-first-battle.png', fullPage: true }); const twentyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2399,7 +2443,7 @@ try { } await page.screenshot({ path: 'dist/verification-redcliffs-fire-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-redcliffs-fire-story.png', fullPage: true }); @@ -2416,8 +2460,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-second-battle-red-cliffs-fire' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-second-battle-red-cliffs-fire' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-second-battle-red-cliffs-fire'); await page.screenshot({ path: 'dist/verification-twenty-second-battle.png', fullPage: true }); const twentySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2535,7 +2580,7 @@ try { } await page.screenshot({ path: 'dist/verification-jingzhou-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jingzhou-story.png', fullPage: true }); @@ -2552,8 +2597,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-third-battle-jingzhou-south-entry' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-third-battle-jingzhou-south-entry' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-third-battle-jingzhou-south-entry'); await page.screenshot({ path: 'dist/verification-twenty-third-battle.png', fullPage: true }); const twentyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2673,7 +2719,7 @@ try { } await page.screenshot({ path: 'dist/verification-guiyang-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-guiyang-story.png', fullPage: true }); @@ -2690,8 +2736,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fourth-battle-guiyang-persuasion' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fourth-battle-guiyang-persuasion' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-fourth-battle-guiyang-persuasion'); await page.screenshot({ path: 'dist/verification-twenty-fourth-battle.png', fullPage: true }); const twentyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2761,8 +2808,8 @@ try { !wulingSortieState?.sortieVisible || !wulingSortieState.sortiePlan?.objectiveLine?.includes('무릉 산길 확보전') || !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recruited && unit.recommended) || - !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && !unit.recommended) || - !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && unit.recommended) || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && unit.recommended) || + !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && !unit.recommended) || !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || @@ -2770,7 +2817,7 @@ try { wulingSortieState.sortieRoster?.length < 10 || wulingSortieState.sortiePlan?.maxCount !== 6 ) { - throw new Error(`Expected twenty-fourth camp sortie prep to target Wuling with Mi Zhu recommended, Yi Ji available, and Gong Zhi absent before victory: ${JSON.stringify(wulingSortieState)}`); + throw new Error(`Expected twenty-fourth camp sortie prep to target Wuling with Yi Ji recommended, Mi Zhu available, and Gong Zhi absent before victory: ${JSON.stringify(wulingSortieState)}`); } assertSortieTacticalRoster(wulingSortieState, [ 'liu-bei', @@ -2785,7 +2832,7 @@ try { 'yi-ji' ]); - const wulingPriorityUnits = ['ma-liang', 'zhuge-liang', 'zhao-yun', 'zhang-fei', 'mi-zhu']; + const wulingPriorityUnits = ['ma-liang', 'zhuge-liang', 'zhao-yun', 'zhang-fei', 'yi-ji']; for (const unitId of wulingPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -2807,11 +2854,11 @@ try { wulingSortieReadyState.sortiePlan?.selectedCount !== 6 || wulingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { - throw new Error(`Expected Wuling sortie to deploy Ma Liang, Mi Zhu, and the mountain-road priority officers while preserving six-officer pressure: ${JSON.stringify(wulingSortieReadyState)}`); + throw new Error(`Expected Wuling sortie to deploy Ma Liang, Yi Ji, and the mountain-road priority officers while preserving six-officer pressure: ${JSON.stringify(wulingSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-wuling-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-wuling-story.png', fullPage: true }); @@ -2828,8 +2875,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fifth-battle-wuling-mountain-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fifth-battle-wuling-mountain-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-fifth-battle-wuling-mountain-road'); await page.screenshot({ path: 'dist/verification-twenty-fifth-battle.png', fullPage: true }); const twentyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2846,7 +2894,7 @@ try { !twentyFifthEnemyBehaviors.has('hold') || !twentyFifthEnemies.some((unit) => unit.id === 'wuling-leader-jin-xuan') || !twentyFifthAllies.some((unit) => unit.id === 'ma-liang') || - !twentyFifthAllies.some((unit) => unit.id === 'mi-zhu') || + !twentyFifthAllies.some((unit) => unit.id === 'yi-ji') || !twentyFifthAllies.some((unit) => unit.id === 'zhang-fei') || !twentyFifthAllies.some((unit) => unit.id === 'zhao-yun') || twentyFifthAllies.some((unit) => unit.id === 'gong-zhi') @@ -2900,7 +2948,7 @@ try { !changshaSortieState?.sortieVisible || !changshaSortieState.sortiePlan?.objectiveLine?.includes('장사 노장 대면전') || !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'gong-zhi' && unit.recruited && unit.recommended) || - !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && unit.recommended) || + !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || @@ -2925,7 +2973,7 @@ try { 'gong-zhi' ]); - const changshaPriorityUnits = ['gong-zhi', 'yi-ji', 'zhuge-liang', 'guan-yu', 'zhao-yun']; + const changshaPriorityUnits = ['gong-zhi', 'zhang-fei', 'zhuge-liang', 'guan-yu', 'zhao-yun']; for (const unitId of changshaPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -2951,7 +2999,7 @@ try { } await page.screenshot({ path: 'dist/verification-changsha-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-changsha-story.png', fullPage: true }); @@ -2968,8 +3016,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-sixth-battle-changsha-veteran' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-sixth-battle-changsha-veteran' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-sixth-battle-changsha-veteran'); await page.screenshot({ path: 'dist/verification-twenty-sixth-battle.png', fullPage: true }); const twentySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -2988,11 +3037,11 @@ try { !twentySixthEnemies.some((unit) => unit.id === 'changsha-veteran-huang-zhong') || !twentySixthEnemies.some((unit) => unit.id === 'changsha-officer-wei-yan') || !twentySixthAllies.some((unit) => unit.id === 'gong-zhi') || - !twentySixthAllies.some((unit) => unit.id === 'yi-ji') || + !twentySixthAllies.some((unit) => unit.id === 'zhang-fei') || !unitUsesTexture(twentySixthBattleState, 'changsha-veteran-huang-zhong', 'unit-huang-zhong') || !unitUsesTexture(twentySixthBattleState, 'changsha-officer-wei-yan', 'unit-wei-yan') || !unitUsesTexture(twentySixthBattleState, 'gong-zhi', 'unit-gong-zhi') || - !unitUsesTexture(twentySixthBattleState, 'yi-ji', 'unit-yi-ji') || + !unitUsesTexture(twentySixthBattleState, 'zhang-fei', 'unit-zhang-fei') || twentySixthAllies.some((unit) => unit.id === 'huang-zhong') || twentySixthAllies.some((unit) => unit.id === 'wei-yan') ) { @@ -3099,7 +3148,7 @@ try { } await page.screenshot({ path: 'dist/verification-yizhou-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yizhou-story.png', fullPage: true }); @@ -3116,8 +3165,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-seventh-battle-yizhou-relief-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-seventh-battle-yizhou-relief-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-seventh-battle-yizhou-relief-road'); await page.screenshot({ path: 'dist/verification-twenty-seventh-battle.png', fullPage: true }); const twentySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -3190,9 +3240,10 @@ try { !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited && unit.recommended) || !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && unit.recommended) || fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng') || fuPassSortieState.sortieRoster?.length < 14 || - fuPassSortieState.sortiePlan?.maxCount !== 6 + fuPassSortieState.sortiePlan?.maxCount !== 7 ) { throw new Error(`Expected twenty-seventh camp sortie prep to target Fu Pass with Pang Tong selectable and Fa Zheng absent before victory: ${JSON.stringify(fuPassSortieState)}`); } @@ -3213,7 +3264,7 @@ try { 'pang-tong' ]); - const fuPassPriorityUnits = ['pang-tong', 'zhuge-liang', 'wei-yan', 'huang-zhong', 'zhao-yun']; + const fuPassPriorityUnits = ['pang-tong', 'zhuge-liang', 'wei-yan', 'huang-zhong', 'zhao-yun', 'mi-zhu']; for (const unitId of fuPassPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -3232,14 +3283,14 @@ try { !fuPassPriorityUnits.every((unitId) => fuPassSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || - fuPassSortieReadyState.sortiePlan?.selectedCount !== 6 || - fuPassSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + fuPassSortieReadyState.sortiePlan?.selectedCount !== 7 || + fuPassSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ) { - throw new Error(`Expected Fu Pass sortie to deploy Pang Tong, Zhuge Liang, Wei Yan, Huang Zhong, and Zhao Yun while preserving six-officer pressure: ${JSON.stringify(fuPassSortieReadyState)}`); + throw new Error(`Expected Fu Pass sortie to deploy Pang Tong, Zhuge Liang, Wei Yan, Huang Zhong, Zhao Yun, and Mi Zhu across seven slots: ${JSON.stringify(fuPassSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-fupass-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fupass-story.png', fullPage: true }); @@ -3256,8 +3307,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-eighth-battle-fu-pass-entry' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-eighth-battle-fu-pass-entry' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-eighth-battle-fu-pass-entry'); await page.screenshot({ path: 'dist/verification-twenty-eighth-battle.png', fullPage: true }); const twentyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -3276,6 +3328,7 @@ try { !twentyEighthAllies.some((unit) => unit.id === 'pang-tong') || !twentyEighthAllies.some((unit) => unit.id === 'huang-zhong') || !twentyEighthAllies.some((unit) => unit.id === 'wei-yan') || + !twentyEighthAllies.some((unit) => unit.id === 'mi-zhu') || twentyEighthAllies.some((unit) => unit.id === 'fa-zheng') ) { throw new Error(`Expected twenty-eighth battle to use Fu Pass map, Gao Pei objective, Pang Tong as selectable ally, mixed AI, and no Fa Zheng before victory: ${JSON.stringify(twentyEighthBattleState)}`); @@ -3328,7 +3381,7 @@ try { !luoSortieState.sortiePlan?.objectiveLine?.includes('낙성 외곽전') || !luoSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !luoSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && !unit.recommended) || - !luoSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && unit.recommended) || + !luoSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited && unit.recommended) || !luoSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !luoSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || !luoSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || @@ -3356,7 +3409,7 @@ try { 'fa-zheng' ]); - const luoPriorityUnits = ['fa-zheng', 'mi-zhu', 'zhuge-liang', 'huang-zhong', 'zhao-yun']; + const luoPriorityUnits = ['fa-zheng', 'wei-yan', 'zhuge-liang', 'huang-zhong', 'zhao-yun']; for (const unitId of luoPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -3378,11 +3431,11 @@ try { luoSortieReadyState.sortiePlan?.selectedCount !== 6 || luoSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { - throw new Error(`Expected Luo Castle sortie to deploy Fa Zheng, Mi Zhu, Zhuge Liang, Huang Zhong, and Zhao Yun while preserving six-officer pressure: ${JSON.stringify(luoSortieReadyState)}`); + throw new Error(`Expected Luo Castle sortie to deploy Fa Zheng, Wei Yan, Zhuge Liang, Huang Zhong, and Zhao Yun while preserving six-officer pressure: ${JSON.stringify(luoSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-luo-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-luo-story.png', fullPage: true }); @@ -3399,8 +3452,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'twenty-ninth-battle-luo-outer-wall' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-ninth-battle-luo-outer-wall' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'twenty-ninth-battle-luo-outer-wall'); await page.screenshot({ path: 'dist/verification-twenty-ninth-battle.png', fullPage: true }); const twentyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -3418,14 +3472,14 @@ try { !twentyNinthEnemies.some((unit) => unit.id === 'luo-leader-zhang-ren') || !twentyNinthEnemies.some((unit) => unit.id === 'luo-officer-wu-yi') || !twentyNinthAllies.some((unit) => unit.id === 'fa-zheng') || - !twentyNinthAllies.some((unit) => unit.id === 'mi-zhu') || + !twentyNinthAllies.some((unit) => unit.id === 'wei-yan') || !twentyNinthAllies.some((unit) => unit.id === 'zhuge-liang') || !unitUsesTexture(twentyNinthBattleState, 'luo-officer-wu-yi', 'unit-wu-yi') || !unitUsesTexture(twentyNinthBattleState, 'fa-zheng', 'unit-fa-zheng') || twentyNinthAllies.some((unit) => unit.id === 'pang-tong') || twentyNinthAllies.some((unit) => unit.id === 'wu-yi') ) { - throw new Error(`Expected twenty-ninth battle to use Luo Castle map, Zhang Ren objective, Wu Yi as enemy officer, Fa Zheng and Mi Zhu selected, mixed AI, and no Pang Tong or Wu Yi before victory: ${JSON.stringify(twentyNinthBattleState)}`); + throw new Error(`Expected twenty-ninth battle to use Luo Castle map, Zhang Ren objective, Wu Yi as enemy officer, Fa Zheng and Wei Yan selected, mixed AI, and no Pang Tong or Wu Yi before victory: ${JSON.stringify(twentyNinthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -3528,7 +3582,7 @@ try { } await page.screenshot({ path: 'dist/verification-luofeng-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-luofeng-story.png', fullPage: true }); @@ -3545,8 +3599,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirtieth-battle-luofeng-ambush'); await page.screenshot({ path: 'dist/verification-thirtieth-battle.png', fullPage: true }); const thirtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -3669,7 +3724,7 @@ try { } await page.screenshot({ path: 'dist/verification-luo-main-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-luo-main-story.png', fullPage: true }); @@ -3686,8 +3741,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-first-battle-luo-main-gate' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-first-battle-luo-main-gate' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-first-battle-luo-main-gate'); await page.screenshot({ path: 'dist/verification-thirty-first-battle.png', fullPage: true }); const thirtyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -3821,7 +3877,7 @@ try { } await page.screenshot({ path: 'dist/verification-mianzhu-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-mianzhu-story.png', fullPage: true }); @@ -3838,8 +3894,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-second-battle-mianzhu-gate' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-second-battle-mianzhu-gate' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-second-battle-mianzhu-gate'); await page.screenshot({ path: 'dist/verification-thirty-second-battle.png', fullPage: true }); const thirtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -3969,7 +4026,7 @@ try { } await page.screenshot({ path: 'dist/verification-chengdu-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-chengdu-story.png', fullPage: true }); @@ -3986,8 +4043,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-third-battle-chengdu-surrender' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-third-battle-chengdu-surrender' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-third-battle-chengdu-surrender'); await page.screenshot({ path: 'dist/verification-thirty-third-battle.png', fullPage: true }); const thirtyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -4118,7 +4176,7 @@ try { } await page.screenshot({ path: 'dist/verification-jiameng-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jiameng-story.png', fullPage: true }); @@ -4135,8 +4193,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fourth-battle-jiameng-pass' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fourth-battle-jiameng-pass' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-fourth-battle-jiameng-pass'); await page.screenshot({ path: 'dist/verification-thirty-fourth-battle.png', fullPage: true }); const thirtyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -4272,7 +4331,7 @@ try { } await page.screenshot({ path: 'dist/verification-yangping-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yangping-story.png', fullPage: true }); @@ -4289,8 +4348,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fifth-battle-yangping-scout' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fifth-battle-yangping-scout' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-fifth-battle-yangping-scout'); await page.screenshot({ path: 'dist/verification-thirty-fifth-battle.png', fullPage: true }); const thirtyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -4423,7 +4483,7 @@ try { } await page.screenshot({ path: 'dist/verification-dingjun-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-dingjun-story.png', fullPage: true }); @@ -4440,8 +4500,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-sixth-battle-dingjun-vanguard'); await page.screenshot({ path: 'dist/verification-thirty-sixth-battle.png', fullPage: true }); const thirtySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -4575,7 +4636,7 @@ try { } await page.screenshot({ path: 'dist/verification-hanzhong-decisive-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-hanzhong-decisive-story.png', fullPage: true }); @@ -4592,8 +4653,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-seventh-battle-hanzhong-decisive' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-seventh-battle-hanzhong-decisive' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-seventh-battle-hanzhong-decisive'); await page.screenshot({ path: 'dist/verification-thirty-seventh-battle.png', fullPage: true }); const thirtySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -4670,7 +4732,7 @@ try { throw new Error(`Expected King of Hanzhong council to be available from the post-Hanzhong sortie panel: ${JSON.stringify(hanzhongKingSortieState)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-hanzhong-king-council-story.png', fullPage: true }); @@ -4724,7 +4786,7 @@ try { throw new Error(`Expected Shu-Han foundation story to be available from the King of Hanzhong camp: ${JSON.stringify(shuHanFoundationSortieState)}`); } - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-shu-han-foundation-story.png', fullPage: true }); @@ -4831,7 +4893,7 @@ try { } await page.screenshot({ path: 'dist/verification-jing-defense-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jing-defense-story.png', fullPage: true }); @@ -4848,8 +4910,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-eighth-battle-jing-defense' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-eighth-battle-jing-defense' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-eighth-battle-jing-defense'); await page.screenshot({ path: 'dist/verification-thirty-eighth-battle.png', fullPage: true }); const thirtyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -4978,7 +5041,7 @@ try { } await page.screenshot({ path: 'dist/verification-fan-castle-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fan-castle-story.png', fullPage: true }); @@ -4995,8 +5058,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'thirty-ninth-battle-fan-castle-vanguard' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'thirty-ninth-battle-fan-castle-vanguard' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'thirty-ninth-battle-fan-castle-vanguard'); await page.screenshot({ path: 'dist/verification-thirty-ninth-battle.png', fullPage: true }); const thirtyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -5125,7 +5189,7 @@ try { } await page.screenshot({ path: 'dist/verification-han-river-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-han-river-story.png', fullPage: true }); @@ -5142,8 +5206,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fortieth-battle-han-river-flood' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fortieth-battle-han-river-flood' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fortieth-battle-han-river-flood'); await page.screenshot({ path: 'dist/verification-fortieth-battle.png', fullPage: true }); const fortiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -5273,7 +5338,7 @@ try { } await page.screenshot({ path: 'dist/verification-fan-siege-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fan-siege-story.png', fullPage: true }); @@ -5290,8 +5355,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-first-battle-fan-castle-siege' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-first-battle-fan-castle-siege' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-first-battle-fan-castle-siege'); await page.screenshot({ path: 'dist/verification-forty-first-battle.png', fullPage: true }); const fortyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -5425,7 +5491,7 @@ try { } await page.screenshot({ path: 'dist/verification-jing-rear-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-jing-rear-story.png', fullPage: true }); @@ -5442,8 +5508,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-second-battle-jing-rear-crisis' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-second-battle-jing-rear-crisis' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-second-battle-jing-rear-crisis'); await page.screenshot({ path: 'dist/verification-forty-second-battle.png', fullPage: true }); const fortySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -5577,7 +5644,7 @@ try { } await page.screenshot({ path: 'dist/verification-gongan-collapse-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-gongan-collapse-story.png', fullPage: true }); @@ -5594,8 +5661,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-third-battle-gongan-collapse' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-third-battle-gongan-collapse' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-third-battle-gongan-collapse'); await page.screenshot({ path: 'dist/verification-forty-third-battle.png', fullPage: true }); const fortyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -5728,7 +5796,7 @@ try { } await page.screenshot({ path: 'dist/verification-maicheng-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-maicheng-story.png', fullPage: true }); @@ -5745,8 +5813,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-fourth-battle-maicheng-isolation' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-fourth-battle-maicheng-isolation' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-fourth-battle-maicheng-isolation'); await page.screenshot({ path: 'dist/verification-forty-fourth-battle.png', fullPage: true }); const fortyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -5879,7 +5948,7 @@ try { } await page.screenshot({ path: 'dist/verification-yiling-vanguard-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yiling-vanguard-story.png', fullPage: true }); @@ -5896,8 +5965,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-fifth-battle-yiling-vanguard' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-fifth-battle-yiling-vanguard' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-fifth-battle-yiling-vanguard'); await page.screenshot({ path: 'dist/verification-forty-fifth-battle.png', fullPage: true }); const fortyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -6030,7 +6100,7 @@ try { } await page.screenshot({ path: 'dist/verification-yiling-fire-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-yiling-fire-story.png', fullPage: true }); @@ -6047,8 +6117,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-sixth-battle-yiling-fire' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-sixth-battle-yiling-fire' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-sixth-battle-yiling-fire'); await page.screenshot({ path: 'dist/verification-forty-sixth-battle.png', fullPage: true }); const fortySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -6127,7 +6198,7 @@ try { } await page.screenshot({ path: 'dist/verification-baidi-entrustment-prep.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-baidi-entrustment-story.png', fullPage: true }); @@ -6243,7 +6314,7 @@ try { } await page.screenshot({ path: 'dist/verification-nanzhong-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-nanzhong-story.png', fullPage: true }); @@ -6260,8 +6331,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-seventh-battle-nanzhong-stabilization' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-seventh-battle-nanzhong-stabilization' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-seventh-battle-nanzhong-stabilization'); await page.screenshot({ path: 'dist/verification-forty-seventh-battle.png', fullPage: true }); const fortySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -6398,7 +6470,7 @@ try { } await page.screenshot({ path: 'dist/verification-meng-huo-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-meng-huo-story.png', fullPage: true }); @@ -6415,8 +6487,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-eighth-battle-meng-huo-main-force' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-eighth-battle-meng-huo-main-force' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-eighth-battle-meng-huo-main-force'); await page.screenshot({ path: 'dist/verification-forty-eighth-battle.png', fullPage: true }); const fortyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -6554,7 +6627,7 @@ try { } await page.screenshot({ path: 'dist/verification-second-capture-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-second-capture-story.png', fullPage: true }); @@ -6571,8 +6644,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'forty-ninth-battle-meng-huo-second-capture' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'forty-ninth-battle-meng-huo-second-capture' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'forty-ninth-battle-meng-huo-second-capture'); await page.screenshot({ path: 'dist/verification-forty-ninth-battle.png', fullPage: true }); const fortyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -6712,7 +6786,7 @@ try { } await page.screenshot({ path: 'dist/verification-third-capture-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-third-capture-story.png', fullPage: true }); @@ -6729,8 +6803,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fiftieth-battle-meng-huo-third-capture' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fiftieth-battle-meng-huo-third-capture' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fiftieth-battle-meng-huo-third-capture'); await page.screenshot({ path: 'dist/verification-fiftieth-battle.png', fullPage: true }); const fiftiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -6871,7 +6946,7 @@ try { } await page.screenshot({ path: 'dist/verification-fourth-capture-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fourth-capture-story.png', fullPage: true }); @@ -6888,8 +6963,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-first-battle-meng-huo-fourth-capture' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-first-battle-meng-huo-fourth-capture' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-first-battle-meng-huo-fourth-capture'); await page.screenshot({ path: 'dist/verification-fifty-first-battle.png', fullPage: true }); const fiftyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7031,7 +7107,7 @@ try { } await page.screenshot({ path: 'dist/verification-fifth-capture-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifth-capture-story.png', fullPage: true }); @@ -7048,8 +7124,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-second-battle-meng-huo-fifth-capture' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-second-battle-meng-huo-fifth-capture' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-second-battle-meng-huo-fifth-capture'); await page.screenshot({ path: 'dist/verification-fifty-second-battle.png', fullPage: true }); const fiftySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7190,7 +7267,7 @@ try { } await page.screenshot({ path: 'dist/verification-sixth-capture-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixth-capture-story.png', fullPage: true }); @@ -7207,8 +7284,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-third-battle-meng-huo-sixth-capture' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-third-battle-meng-huo-sixth-capture' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-third-battle-meng-huo-sixth-capture'); await page.screenshot({ path: 'dist/verification-fifty-third-battle.png', fullPage: true }); const fiftyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7349,7 +7427,7 @@ try { } await page.screenshot({ path: 'dist/verification-final-capture-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-final-capture-story.png', fullPage: true }); @@ -7366,8 +7444,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fourth-battle-meng-huo-final-capture' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fourth-battle-meng-huo-final-capture' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-fourth-battle-meng-huo-final-capture'); await page.screenshot({ path: 'dist/verification-fifty-fourth-battle.png', fullPage: true }); const fiftyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7460,7 +7539,7 @@ try { } await page.screenshot({ path: 'dist/verification-northern-prep-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-northern-prep-story.png', fullPage: true }); @@ -7561,7 +7640,7 @@ try { } await page.screenshot({ path: 'dist/verification-fifty-fifth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-fifth-story.png', fullPage: true }); @@ -7578,8 +7657,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fifth-battle-northern-qishan-road' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fifth-battle-northern-qishan-road' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-fifth-battle-northern-qishan-road'); await page.screenshot({ path: 'dist/verification-fifty-fifth-battle.png', fullPage: true }); const fiftyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7705,7 +7785,7 @@ try { } await page.screenshot({ path: 'dist/verification-fifty-sixth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-sixth-story.png', fullPage: true }); @@ -7722,8 +7802,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-sixth-battle-tianshui-advance' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-sixth-battle-tianshui-advance' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-sixth-battle-tianshui-advance'); await page.screenshot({ path: 'dist/verification-fifty-sixth-battle.png', fullPage: true }); const fiftySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7851,7 +7932,7 @@ try { } await page.screenshot({ path: 'dist/verification-fifty-seventh-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-seventh-story.png', fullPage: true }); @@ -7868,8 +7949,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-seventh-battle-jieting-crisis' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-seventh-battle-jieting-crisis' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-seventh-battle-jieting-crisis'); await page.screenshot({ path: 'dist/verification-fifty-seventh-battle.png', fullPage: true }); const fiftySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -7998,7 +8080,7 @@ try { } await page.screenshot({ path: 'dist/verification-fifty-eighth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-eighth-story.png', fullPage: true }); @@ -8015,8 +8097,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-eighth-battle-qishan-retreat' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-eighth-battle-qishan-retreat' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-eighth-battle-qishan-retreat'); await page.screenshot({ path: 'dist/verification-fifty-eighth-battle.png', fullPage: true }); const fiftyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -8143,7 +8226,7 @@ try { } await page.screenshot({ path: 'dist/verification-fifty-ninth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-fifty-ninth-story.png', fullPage: true }); @@ -8160,8 +8243,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'fifty-ninth-battle-chencang-siege' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'fifty-ninth-battle-chencang-siege' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'fifty-ninth-battle-chencang-siege'); await page.screenshot({ path: 'dist/verification-fifty-ninth-battle.png', fullPage: true }); const fiftyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -8244,7 +8328,7 @@ try { !String(wuduSortieState.sortiePlan?.objectiveLine ?? '').includes('무도·음평 확보전') || !String(wuduSortieState.sortiePlan?.objectiveLine ?? '').includes('곽회') || wuduSortieState.sortiePlan?.maxCount !== 7 || - wuduSortieState.sortiePlan?.recommendedTotal !== 8 || + wuduSortieState.sortiePlan?.recommendedTotal !== 7 || !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || @@ -8252,16 +8336,16 @@ try { !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || - !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + !wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && !unit.recommended) || wuduSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Wudu/Yinping sortie to require Zhuge Liang, exclude Liu Bei, and recommend eight competing northern roles: ${JSON.stringify(wuduSortieState)}`); + throw new Error(`Expected Wudu/Yinping sortie to require Zhuge Liang, exclude Liu Bei, and recommend seven northern roles: ${JSON.stringify(wuduSortieState)}`); } assertSortieTacticalRoster(wuduSortieState, [ 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei' ]); - const wuduPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang']; + const wuduPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-dai']; for (const unitId of wuduPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -8282,16 +8366,16 @@ try { ) || !wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || - wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) || + wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) || wuduSortieReadyState.sortiePlan?.selectedCount !== 7 || wuduSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - wuduSortieReadyState.sortiePlan?.recommendedTotal !== 8 + wuduSortieReadyState.sortiePlan?.recommendedTotal !== 7 ) { - throw new Error(`Expected Wudu/Yinping sortie to deploy Zhuge Liang, Jiang Wei, Wei Yan, Zhao Yun, Wang Ping, Huang Quan, and Ma Liang while benching one recommended cavalry role: ${JSON.stringify(wuduSortieReadyState)}`); + throw new Error(`Expected Wudu/Yinping sortie to deploy Zhuge Liang, Jiang Wei, Wei Yan, Zhao Yun, Wang Ping, Huang Quan, and Ma Dai: ${JSON.stringify(wuduSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixtieth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixtieth-story.png', fullPage: true }); @@ -8308,8 +8392,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixtieth-battle-wudu-yinping' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixtieth-battle-wudu-yinping' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixtieth-battle-wudu-yinping'); await page.screenshot({ path: 'dist/verification-sixtieth-battle.png', fullPage: true }); const sixtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -8338,11 +8423,11 @@ try { !sixtiethAllies.some((unit) => unit.id === 'zhao-yun') || !sixtiethAllies.some((unit) => unit.id === 'wang-ping') || !sixtiethAllies.some((unit) => unit.id === 'huang-quan') || - !sixtiethAllies.some((unit) => unit.id === 'ma-liang') || - sixtiethAllies.some((unit) => unit.id === 'ma-dai') || + !sixtiethAllies.some((unit) => unit.id === 'ma-dai') || + sixtiethAllies.some((unit) => unit.id === 'ma-liang') || sixtiethAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected sixtieth battle to use the Wudu/Yinping map, Guo Huai objectives, a deliberately benched recommended cavalry role, and mixed Wei AI: ${JSON.stringify(sixtiethBattleState)}`); + throw new Error(`Expected sixtieth battle to use the Wudu/Yinping map, Guo Huai objectives, the recommended Ma Dai cavalry role, and mixed Wei AI: ${JSON.stringify(sixtiethBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -8394,8 +8479,8 @@ try { hanzhongRainSortieState.campTitle !== '무도·음평 확보 후 군영' || !String(hanzhongRainSortieState.sortiePlan?.objectiveLine ?? '').includes('한중 우로 방어전') || !String(hanzhongRainSortieState.sortiePlan?.objectiveLine ?? '').includes('보급선') || - hanzhongRainSortieState.sortiePlan?.maxCount !== 7 || - hanzhongRainSortieState.sortiePlan?.recommendedTotal !== 9 || + hanzhongRainSortieState.sortiePlan?.maxCount !== 8 || + hanzhongRainSortieState.sortiePlan?.recommendedTotal !== 8 || !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || @@ -8404,16 +8489,16 @@ try { !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || - !hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || + hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Hanzhong rainy-pass sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing defense/logistics roles: ${JSON.stringify(hanzhongRainSortieState)}`); + throw new Error(`Expected Hanzhong rainy-pass sortie to require Zhuge Liang, exclude Liu Bei, recommend eight defense/logistics roles, and leave Ma Dai non-recommended: ${JSON.stringify(hanzhongRainSortieState)}`); } assertSortieTacticalRoster(hanzhongRainSortieState, [ 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei' ]); - const hanzhongRainPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'ma-liang', 'wei-yan']; + const hanzhongRainPriorityUnits = ['jiang-wei', 'wei-yan', 'li-yan', 'huang-quan', 'wang-ping', 'zhao-yun', 'ma-liang']; for (const unitId of hanzhongRainPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -8434,17 +8519,16 @@ try { ) || !hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || - hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) || hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) || - hanzhongRainSortieReadyState.sortiePlan?.selectedCount !== 7 || - hanzhongRainSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - hanzhongRainSortieReadyState.sortiePlan?.recommendedTotal !== 9 + hanzhongRainSortieReadyState.sortiePlan?.selectedCount !== 8 || + hanzhongRainSortieReadyState.sortiePlan?.recommendedSelectedCount !== 8 || + hanzhongRainSortieReadyState.sortiePlan?.recommendedTotal !== 8 ) { - throw new Error(`Expected Hanzhong rainy-pass sortie to deploy Zhuge Liang plus six logistics/road officers while benching two recommended cavalry roles: ${JSON.stringify(hanzhongRainSortieReadyState)}`); + throw new Error(`Expected Hanzhong rainy-pass sortie to deploy all eight recommended defense/logistics officers while benching Ma Dai: ${JSON.stringify(hanzhongRainSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixty-first-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixty-first-story.png', fullPage: true }); @@ -8461,8 +8545,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixty-first-battle-hanzhong-rain-defense' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixty-first-battle-hanzhong-rain-defense' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixty-first-battle-hanzhong-rain-defense'); await page.screenshot({ path: 'dist/verification-sixty-first-battle.png', fullPage: true }); const sixtyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -8492,11 +8577,11 @@ try { !sixtyFirstAllies.some((unit) => unit.id === 'li-yan') || !sixtyFirstAllies.some((unit) => unit.id === 'ma-liang') || !sixtyFirstAllies.some((unit) => unit.id === 'wei-yan') || - sixtyFirstAllies.some((unit) => unit.id === 'zhao-yun') || + !sixtyFirstAllies.some((unit) => unit.id === 'zhao-yun') || sixtyFirstAllies.some((unit) => unit.id === 'ma-dai') || sixtyFirstAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected sixty-first battle to use the Hanzhong rainy-pass map, Cao Zhen/Sima Yi objectives, logistics-heavy selected sortie, and mixed Wei AI: ${JSON.stringify(sixtyFirstBattleState)}`); + throw new Error(`Expected sixty-first battle to use the Hanzhong rainy-pass map, Cao Zhen/Sima Yi objectives, all eight recommended defense/logistics officers, and mixed Wei AI: ${JSON.stringify(sixtyFirstBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -8548,8 +8633,8 @@ try { qishanRenewedSortieState.campTitle !== '한중 우로 방어 후 군영' || !String(qishanRenewedSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 재출정전') || !String(qishanRenewedSortieState.sortiePlan?.objectiveLine ?? '').includes('노성 지연선') || - qishanRenewedSortieState.sortiePlan?.maxCount !== 7 || - qishanRenewedSortieState.sortiePlan?.recommendedTotal !== 9 || + qishanRenewedSortieState.sortiePlan?.maxCount !== 8 || + qishanRenewedSortieState.sortiePlan?.recommendedTotal !== 8 || !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || @@ -8557,17 +8642,17 @@ try { !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || - !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) || !qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || + qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) || qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Qishan renewed sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing offensive/return-line roles: ${JSON.stringify(qishanRenewedSortieState)}`); + throw new Error(`Expected Qishan renewed sortie to require Zhuge Liang, exclude Liu Bei, recommend eight offensive/return-line roles, and leave Li Yan non-recommended: ${JSON.stringify(qishanRenewedSortieState)}`); } assertSortieTacticalRoster(qishanRenewedSortieState, [ 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei' ]); - const qishanRenewedPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang']; + const qishanRenewedPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang', 'ma-dai']; for (const unitId of qishanRenewedPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -8589,16 +8674,15 @@ try { !qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.selected) || - qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) || - qishanRenewedSortieReadyState.sortiePlan?.selectedCount !== 7 || - qishanRenewedSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - qishanRenewedSortieReadyState.sortiePlan?.recommendedTotal !== 9 + qishanRenewedSortieReadyState.sortiePlan?.selectedCount !== 8 || + qishanRenewedSortieReadyState.sortiePlan?.recommendedSelectedCount !== 8 || + qishanRenewedSortieReadyState.sortiePlan?.recommendedTotal !== 8 ) { - throw new Error(`Expected Qishan renewed sortie to deploy Zhuge Liang plus six forward/return-line officers while benching Li Yan and Ma Dai: ${JSON.stringify(qishanRenewedSortieReadyState)}`); + throw new Error(`Expected Qishan renewed sortie to deploy all eight recommended forward/return-line officers while benching Li Yan: ${JSON.stringify(qishanRenewedSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixty-second-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); await page.screenshot({ path: 'dist/verification-sixty-second-story.png', fullPage: true }); @@ -8615,8 +8699,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixty-second-battle-qishan-renewed-offensive' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixty-second-battle-qishan-renewed-offensive' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixty-second-battle-qishan-renewed-offensive'); await page.screenshot({ path: 'dist/verification-sixty-second-battle.png', fullPage: true }); const sixtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -8650,10 +8735,10 @@ try { !sixtySecondAllies.some((unit) => unit.id === 'huang-quan') || !sixtySecondAllies.some((unit) => unit.id === 'ma-liang') || sixtySecondAllies.some((unit) => unit.id === 'li-yan') || - sixtySecondAllies.some((unit) => unit.id === 'ma-dai') || + !sixtySecondAllies.some((unit) => unit.id === 'ma-dai') || sixtySecondAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected sixty-second battle to use the Qishan renewed-offensive map, Sima Yi/Zhang He/Guo Huai objectives, selected forward/return-line officers, and mixed Wei AI: ${JSON.stringify(sixtySecondBattleState)}`); + throw new Error(`Expected sixty-second battle to use the Qishan renewed-offensive map, Sima Yi/Zhang He/Guo Huai objectives, all eight recommended forward/return-line officers, and mixed Wei AI: ${JSON.stringify(sixtySecondBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -8705,26 +8790,26 @@ try { luchengPursuitSortieState.campTitle !== '기산 재출정 후 군영' || !String(luchengPursuitSortieState.sortiePlan?.objectiveLine ?? '').includes('노성 추격전') || !String(luchengPursuitSortieState.sortiePlan?.objectiveLine ?? '').includes('회수 표식') || - luchengPursuitSortieState.sortiePlan?.maxCount !== 7 || - luchengPursuitSortieState.sortiePlan?.recommendedTotal !== 9 || + luchengPursuitSortieState.sortiePlan?.maxCount !== 8 || + luchengPursuitSortieState.sortiePlan?.recommendedTotal !== 8 || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || - !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || !luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || + luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) || luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Lucheng pursuit sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing pursuit/return/supply roles: ${JSON.stringify(luchengPursuitSortieState)}`); + throw new Error(`Expected Lucheng pursuit sortie to require Zhuge Liang, exclude Liu Bei, recommend eight pursuit/return/supply roles, and leave Li Yan non-recommended: ${JSON.stringify(luchengPursuitSortieState)}`); } assertSortieTacticalRoster(luchengPursuitSortieState, [ 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei' ]); - const luchengPursuitPriorityUnits = ['jiang-wei', 'zhao-yun', 'wei-yan', 'wang-ping', 'huang-quan', 'li-yan']; + const luchengPursuitPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang', 'ma-dai']; for (const unitId of luchengPursuitPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -8745,17 +8830,16 @@ try { ) || !luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || - luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) || - luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) || - luchengPursuitSortieReadyState.sortiePlan?.selectedCount !== 7 || - luchengPursuitSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - luchengPursuitSortieReadyState.sortiePlan?.recommendedTotal !== 9 + luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.selected) || + luchengPursuitSortieReadyState.sortiePlan?.selectedCount !== 8 || + luchengPursuitSortieReadyState.sortiePlan?.recommendedSelectedCount !== 8 || + luchengPursuitSortieReadyState.sortiePlan?.recommendedTotal !== 8 ) { - throw new Error(`Expected Lucheng pursuit sortie to deploy Zhuge Liang plus six pursuit/return/supply officers while benching Ma Liang and Ma Dai: ${JSON.stringify(luchengPursuitSortieReadyState)}`); + throw new Error(`Expected Lucheng pursuit sortie to deploy all eight recommended pursuit/return/supply officers while benching Li Yan: ${JSON.stringify(luchengPursuitSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixty-third-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 12; i += 1) { const storyState = await page.evaluate(() => @@ -8795,8 +8879,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixty-third-battle-lucheng-pursuit' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixty-third-battle-lucheng-pursuit' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixty-third-battle-lucheng-pursuit'); await page.screenshot({ path: 'dist/verification-sixty-third-battle.png', fullPage: true }); const sixtyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -8828,12 +8913,12 @@ try { !sixtyThirdAllies.some((unit) => unit.id === 'wei-yan') || !sixtyThirdAllies.some((unit) => unit.id === 'wang-ping') || !sixtyThirdAllies.some((unit) => unit.id === 'huang-quan') || - !sixtyThirdAllies.some((unit) => unit.id === 'li-yan') || - sixtyThirdAllies.some((unit) => unit.id === 'ma-liang') || - sixtyThirdAllies.some((unit) => unit.id === 'ma-dai') || + !sixtyThirdAllies.some((unit) => unit.id === 'ma-liang') || + !sixtyThirdAllies.some((unit) => unit.id === 'ma-dai') || + sixtyThirdAllies.some((unit) => unit.id === 'li-yan') || sixtyThirdAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected sixty-third battle to use the Lucheng pursuit map, Sima Yi/Zhang He/Guo Huai objectives, selected pursuit/return/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyThirdBattleState)}`); + throw new Error(`Expected sixty-third battle to use the Lucheng pursuit map, Sima Yi/Zhang He/Guo Huai objectives, all eight recommended pursuit/return/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyThirdBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -8885,9 +8970,9 @@ try { weishuiCampsSortieState.campTitle !== '노성 추격 후 군영' || !String(weishuiCampsSortieState.sortiePlan?.objectiveLine ?? '').includes('위수 진영전') || !String(weishuiCampsSortieState.sortiePlan?.objectiveLine ?? '').includes('봉화 차단') || - weishuiCampsSortieState.sortiePlan?.maxCount !== 7 || - weishuiCampsSortieState.sortiePlan?.recommendedTotal !== 9 || - !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + weishuiCampsSortieState.sortiePlan?.maxCount !== 8 || + weishuiCampsSortieState.sortiePlan?.recommendedTotal !== 8 || + !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required && unit.recommended) || !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || @@ -8895,16 +8980,16 @@ try { !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || - !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + !weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && !unit.recommended) || weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Weishui camps sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine river/camp/supply roles: ${JSON.stringify(weishuiCampsSortieState)}`); + throw new Error(`Expected Weishui camps sortie to require Zhuge Liang, exclude Liu Bei, and recommend eight river/camp/supply roles including Zhao Yun: ${JSON.stringify(weishuiCampsSortieState)}`); } assertSortieTacticalRoster(weishuiCampsSortieState, [ 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei' ]); - const weishuiCampsPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan', 'ma-dai']; + const weishuiCampsPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan', 'ma-dai', 'zhao-yun']; for (const unitId of weishuiCampsPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -8925,17 +9010,16 @@ try { ) || !weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || - weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) || weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) || - weishuiCampsSortieReadyState.sortiePlan?.selectedCount !== 7 || - weishuiCampsSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - weishuiCampsSortieReadyState.sortiePlan?.recommendedTotal !== 9 + weishuiCampsSortieReadyState.sortiePlan?.selectedCount !== 8 || + weishuiCampsSortieReadyState.sortiePlan?.recommendedSelectedCount !== 8 || + weishuiCampsSortieReadyState.sortiePlan?.recommendedTotal !== 8 ) { - throw new Error(`Expected Weishui camps sortie to deploy Zhuge Liang plus six river/camp/supply officers while benching Zhao Yun and Ma Liang: ${JSON.stringify(weishuiCampsSortieReadyState)}`); + throw new Error(`Expected Weishui camps sortie to deploy Zhuge Liang plus seven recommended river/camp/supply officers including Zhao Yun while benching Ma Liang: ${JSON.stringify(weishuiCampsSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixty-fourth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 12; i += 1) { const storyState = await page.evaluate(() => @@ -8977,8 +9061,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixty-fourth-battle-weishui-camps' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixty-fourth-battle-weishui-camps' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixty-fourth-battle-weishui-camps'); await page.screenshot({ path: 'dist/verification-sixty-fourth-battle.png', fullPage: true }); const sixtyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -9011,11 +9096,11 @@ try { !sixtyFourthAllies.some((unit) => unit.id === 'li-yan') || !sixtyFourthAllies.some((unit) => unit.id === 'wei-yan') || !sixtyFourthAllies.some((unit) => unit.id === 'ma-dai') || - sixtyFourthAllies.some((unit) => unit.id === 'zhao-yun') || + !sixtyFourthAllies.some((unit) => unit.id === 'zhao-yun') || sixtyFourthAllies.some((unit) => unit.id === 'ma-liang') || sixtyFourthAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected sixty-fourth battle to use the Weishui camps map, Sima Yi/Zhang He/Guo Huai objectives, selected river/camp/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyFourthBattleState)}`); + throw new Error(`Expected sixty-fourth battle to use the Weishui camps map, Sima Yi/Zhang He/Guo Huai objectives, all eight recommended river/camp/supply officers including Zhao Yun, and mixed Wei AI: ${JSON.stringify(sixtyFourthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -9067,26 +9152,27 @@ try { northbankSortieState.campTitle !== '위수 진영전 후 군영' || !String(northbankSortieState.sortiePlan?.objectiveLine ?? '').includes('위수 북안 압박전') || !String(northbankSortieState.sortiePlan?.objectiveLine ?? '').includes('재추격 저지') || - northbankSortieState.sortiePlan?.maxCount !== 7 || - northbankSortieState.sortiePlan?.recommendedTotal !== 9 || - !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + northbankSortieState.sortiePlan?.maxCount !== 8 || + northbankSortieState.sortiePlan?.recommendedTotal !== 8 || + !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required && unit.recommended) || !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) || !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) || !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) || - !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) || !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || - !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recommended) || + !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && !unit.recommended) || + !northbankSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && !unit.recommended) || northbankSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Weishui north bank sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine low-road/flank/supply roles: ${JSON.stringify(northbankSortieState)}`); + throw new Error(`Expected Weishui north bank sortie to require Zhuge Liang, exclude Liu Bei, and recommend eight low-road/flank/supply roles including Zhao Yun and Ma Chao: ${JSON.stringify(northbankSortieState)}`); } assertSortieTacticalRoster(northbankSortieState, [ 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei' ]); - const northbankPriorityUnits = ['jiang-wei', 'ma-dai', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan']; + const northbankPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'ma-dai', 'zhao-yun', 'ma-chao']; for (const unitId of northbankPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { @@ -9107,17 +9193,17 @@ try { ) || !northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || - northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) || + northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.selected) || northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) || - northbankSortieReadyState.sortiePlan?.selectedCount !== 7 || - northbankSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - northbankSortieReadyState.sortiePlan?.recommendedTotal !== 9 + northbankSortieReadyState.sortiePlan?.selectedCount !== 8 || + northbankSortieReadyState.sortiePlan?.recommendedSelectedCount !== 8 || + northbankSortieReadyState.sortiePlan?.recommendedTotal !== 8 ) { - throw new Error(`Expected Weishui north bank sortie to deploy Zhuge Liang plus six low-road/flank/supply officers while benching Zhao Yun and Ma Liang: ${JSON.stringify(northbankSortieReadyState)}`); + throw new Error(`Expected Weishui north bank sortie to deploy Zhuge Liang plus seven recommended low-road/flank/supply officers including Zhao Yun and Ma Chao while benching Wei Yan and Ma Liang: ${JSON.stringify(northbankSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixty-fifth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 12; i += 1) { const storyState = await page.evaluate(() => @@ -9166,8 +9252,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixty-fifth-battle-weishui-northbank' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixty-fifth-battle-weishui-northbank' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixty-fifth-battle-weishui-northbank'); await page.screenshot({ path: 'dist/verification-sixty-fifth-battle.png', fullPage: true }); const sixtyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -9199,17 +9286,18 @@ try { !sixtyFifthAllies.some((unit) => unit.id === 'wang-ping') || !sixtyFifthAllies.some((unit) => unit.id === 'huang-quan') || !sixtyFifthAllies.some((unit) => unit.id === 'li-yan') || - !sixtyFifthAllies.some((unit) => unit.id === 'wei-yan') || + !sixtyFifthAllies.some((unit) => unit.id === 'zhao-yun') || + !sixtyFifthAllies.some((unit) => unit.id === 'ma-chao') || !unitUsesTexture(sixtyFifthBattleState, 'ma-dai', 'unit-ma-dai') || !unitUsesTexture(sixtyFifthBattleState, 'wang-ping', 'unit-wang-ping') || !unitUsesTexture(sixtyFifthBattleState, 'huang-quan', 'unit-huang-quan') || !unitUsesTexture(sixtyFifthBattleState, 'li-yan', 'unit-li-yan') || - !unitUsesTexture(sixtyFifthBattleState, 'wei-yan', 'unit-wei-yan') || - sixtyFifthAllies.some((unit) => unit.id === 'zhao-yun') || + !unitUsesTexture(sixtyFifthBattleState, 'ma-chao', 'unit-ma-chao') || + sixtyFifthAllies.some((unit) => unit.id === 'wei-yan') || sixtyFifthAllies.some((unit) => unit.id === 'ma-liang') || sixtyFifthAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected sixty-fifth battle to use the Weishui north bank map, Sima Yi/Zhang He/Guo Huai objectives, selected low-road/flank/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyFifthBattleState)}`); + throw new Error(`Expected sixty-fifth battle to use the Weishui north bank map, Sima Yi/Zhang He/Guo Huai objectives, all eight recommended low-road/flank/supply officers including Zhao Yun and Ma Chao, and mixed Wei AI: ${JSON.stringify(sixtyFifthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -9253,23 +9341,26 @@ try { await page.mouse.click(1160, 38); await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortieVisible === true); - const wuzhangPriorityUnits = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'ma-dai', 'huang-quan', 'li-yan', 'wei-yan']; + const wuzhangPriorityUnits = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'zhao-yun', 'ma-dai', 'ma-chao', 'huang-quan', 'li-yan']; const wuzhangSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !wuzhangPriorityUnits.every((unitId) => - wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected && unit.recommended) ) || - !wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required && unit.recommended) || wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || - wuzhangSortieReadyState.sortiePlan?.selectedCount !== 7 || - wuzhangSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 || - wuzhangSortieReadyState.sortiePlan?.recommendedTotal !== 9 + !wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && !unit.selected && !unit.recommended) || + !wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && !unit.selected && !unit.recommended) || + wuzhangSortieReadyState.sortiePlan?.maxCount !== 8 || + wuzhangSortieReadyState.sortiePlan?.selectedCount !== 8 || + wuzhangSortieReadyState.sortiePlan?.recommendedSelectedCount !== 8 || + wuzhangSortieReadyState.sortiePlan?.recommendedTotal !== 8 ) { - throw new Error(`Expected Wuzhang finale sortie to keep Zhuge Liang plus inherited northern campaign core officers selected: ${JSON.stringify(wuzhangSortieReadyState)}`); + throw new Error(`Expected Wuzhang finale sortie to select all eight recommended officers including Zhao Yun and Ma Chao while benching Wei Yan and Ma Liang: ${JSON.stringify(wuzhangSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-sixty-sixth-sortie.png', fullPage: true }); - await page.mouse.click(1068, 646); + await launchSortieFromCurrentStep(page); await waitForStoryReady(page); for (let i = 0; i < 12; i += 1) { const storyState = await page.evaluate(() => @@ -9306,8 +9397,9 @@ try { } await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); - return state?.scene === 'BattleScene' && state?.battleId === 'sixty-sixth-battle-wuzhang-final' && state?.battleOutcome === null && state?.phase === 'idle' && state?.mapBackgroundReady === true && state?.resultVisible === false; + return state?.scene === 'BattleScene' && state?.battleId === 'sixty-sixth-battle-wuzhang-final' && state?.battleOutcome === null && ['deployment', 'idle'].includes(state?.phase) && state?.mapBackgroundReady === true && state?.resultVisible === false; }); + await startDeploymentIfNeeded(page, 'sixty-sixth-battle-wuzhang-final'); await page.screenshot({ path: 'dist/verification-sixty-sixth-battle.png', fullPage: true }); const sixtySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); @@ -9335,9 +9427,11 @@ try { !unitUsesTexture(sixtySixthBattleState, 'wang-ping', 'unit-wang-ping') || !unitUsesTexture(sixtySixthBattleState, 'huang-quan', 'unit-huang-quan') || !unitUsesTexture(sixtySixthBattleState, 'li-yan', 'unit-li-yan') || + sixtySixthAllies.some((unit) => unit.id === 'wei-yan') || + sixtySixthAllies.some((unit) => unit.id === 'ma-liang') || sixtySixthAllies.some((unit) => unit.id === 'liu-bei') ) { - throw new Error(`Expected Wuzhang finale battle to use the new map, Sima Yi/Zhang He/Guo Huai objectives, and inherited northern campaign officers: ${JSON.stringify(sixtySixthBattleState)}`); + throw new Error(`Expected Wuzhang finale battle to use the new map, Sima Yi/Zhang He/Guo Huai objectives, all eight recommended officers including Zhao Yun and Ma Chao, and no Wei Yan or Ma Liang: ${JSON.stringify(sixtySixthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); @@ -9347,43 +9441,7 @@ try { }); await page.mouse.click(738, 642); - await waitForCampReady(page); - - const sixtySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); - if ( - sixtySixthCampState?.campBattleId !== 'sixty-sixth-battle-wuzhang-final' || - sixtySixthCampState.campaign?.step !== 'sixty-sixth-camp' || - sixtySixthCampState.campTitle !== '오장원 최종전 후 군영' || - sixtySixthCampState.availableDialogueIds?.length !== 2 || - !sixtySixthCampState.availableDialogueIds.every((id) => id.startsWith('northern-twelfth-')) || - sixtySixthCampState.availableVisitIds?.length !== 1 || - !sixtySixthCampState.availableVisitIds.every((id) => id.startsWith('northern-twelfth-')) || - sixtySixthCampState.rosterCollection?.total < 22 || - !sixtySixthCampState.report?.bonds?.some((bond) => bond.id === 'zhuge-liang__jiang-wei_northern-twelfth') - ) { - throw new Error(`Expected Wuzhang finale aftermath camp to expose twelfth northern campaign events and bonds: ${JSON.stringify(sixtySixthCampState)}`); - } - await page.screenshot({ path: 'dist/verification-sixty-sixth-camp.png', fullPage: true }); - - await page.mouse.click(966, 38); - await page.waitForTimeout(180); - const postSixtySixthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); - if ( - postSixtySixthProgressState.campaignProgress?.completedKnown !== 66 || - postSixtySixthProgressState.campaignProgress?.totalKnown !== 66 || - postSixtySixthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' || - postSixtySixthProgressState.campaignProgress?.latestBattleTitle !== '오장원 최종전' || - postSixtySixthProgressState.campaignProgress?.nextBattleTitle !== '북벌의 끝과 남은 뜻' - ) { - throw new Error(`Expected post-sixty-sixth progress tab to complete Wuzhang finale and expose the final epilogue: ${JSON.stringify(postSixtySixthProgressState?.campaignProgress)}`); - } - await page.screenshot({ path: 'dist/verification-post-sixty-sixth-progress.png', fullPage: true }); - - await page.mouse.click(1160, 38); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - return activeScenes.includes('StoryScene'); - }); + await waitForStoryReady(page); await page.waitForFunction(() => { const story = window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.(); return ( @@ -9556,6 +9614,31 @@ async function canReach(url) { } } +async function startDeploymentIfNeeded(page, battleId) { + const state = await page.evaluate((expectedBattleId) => { + const battle = window.__HEROS_DEBUG__?.battle(); + return battle?.battleId === expectedBattleId ? battle : undefined; + }, battleId); + + if (!state || !['deployment', 'idle'].includes(state.phase)) { + throw new Error(`Expected ${battleId} to be ready for deployment: ${JSON.stringify(state)}`); + } + if (state.phase === 'idle') { + await page.waitForTimeout(250); + return; + } + if (!Array.isArray(state.deployedAllyIds) || state.deployedAllyIds.length === 0) { + throw new Error(`Expected ${battleId} deployment to contain allied officers: ${JSON.stringify(state)}`); + } + + await page.mouse.click(1085, 637); + await page.waitForFunction((expectedBattleId) => { + const battle = window.__HEROS_DEBUG__?.battle(); + return battle?.battleId === expectedBattleId && battle?.phase === 'idle' && battle?.triggeredBattleEvents?.includes('opening'); + }, battleId, { timeout: 30000 }); + await page.waitForTimeout(250); +} + async function waitForStoryReady(page) { await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -9584,6 +9667,25 @@ async function waitForCampReady(page, options = {}) { }, options, { timeout: 90000 }); } +async function waitForCampAfterBattleResult(page) { + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene') || activeScenes.includes('StoryScene'); + }, undefined, { timeout: 90000 }); + + for (let i = 0; i < 48; i += 1) { + const activeScenes = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []); + if (activeScenes.includes('CampScene')) { + await waitForCampReady(page); + return; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(240); + } + + await waitForCampReady(page); +} + function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -9783,12 +9885,92 @@ async function openSortiePrep(page) { throw new Error(`Cannot open sortie prep: ${JSON.stringify(state)}`); } +async function ensureSortiePrepStep(page, expectedStep) { + const stepOrder = ['briefing', 'formation', 'loadout']; + const expectedIndex = stepOrder.indexOf(expectedStep); + if (expectedIndex < 0) { + throw new Error(`Unknown sortie preparation step: ${expectedStep}`); + } + + let state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (!state?.sortieVisible) { + throw new Error(`Cannot enter sortie step ${expectedStep}; sortie preparation is not visible: ${JSON.stringify(state)}`); + } + if (!state.stagedSortiePrep) { + return state; + } + + let currentIndex = stepOrder.indexOf(state.sortiePrepStep); + if (currentIndex < 0) { + throw new Error(`Unknown current sortie preparation step: ${JSON.stringify(state)}`); + } + + if (currentIndex > expectedIndex) { + await page.evaluate((step) => { + window.__HEROS_GAME__?.scene.getScene('CampScene')?.setSortiePrepStep?.(step); + }, expectedStep); + await page.waitForFunction((step) => { + const camp = window.__HEROS_DEBUG__?.camp(); + return camp?.sortieVisible === true && camp?.sortiePrepStep === step; + }, expectedStep, { timeout: 30000 }); + return page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + } + + while (currentIndex < expectedIndex) { + const nextStep = stepOrder[currentIndex + 1]; + await page.keyboard.press('Enter'); + await page.waitForFunction((step) => { + const camp = window.__HEROS_DEBUG__?.camp(); + return camp?.sortieVisible === true && camp?.sortiePrepStep === step; + }, nextStep, { timeout: 30000 }); + state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + currentIndex = stepOrder.indexOf(state?.sortiePrepStep); + } + + return state; +} + +async function launchSortieFromCurrentStep(page) { + const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (!state?.sortieVisible) { + throw new Error(`Cannot launch sortie; preparation is not visible: ${JSON.stringify(state)}`); + } + + if (!state.stagedSortiePrep) { + await page.mouse.click(1068, 646); + return; + } + + await ensureSortiePrepStep(page, 'loadout'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + const camp = window.__HEROS_DEBUG__?.camp(); + return !activeScenes.includes('CampScene') || camp?.sortieVisible !== true; + }, undefined, { timeout: 30000 }); +} + async function clickSortieRosterUnit(page, unitId) { + await ensureSortiePrepStep(page, 'formation'); let state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!state?.sortieVisible || !Array.isArray(state.sortieRoster)) { throw new Error(`Cannot click sortie unit ${unitId}; sortie roster is not visible: ${JSON.stringify(state)}`); } + if (state.stagedSortiePrep && state.sortiePortraitRosterView?.enabled) { + if (!state.sortieRoster.some((unit) => unit.id === unitId)) { + throw new Error(`Cannot toggle portrait sortie unit ${unitId}; unit is missing: ${JSON.stringify(state.sortieRoster)}`); + } + await page.evaluate((targetUnitId) => { + window.__HEROS_GAME__?.scene.getScene('CampScene')?.toggleSortieUnit?.(targetUnitId); + }, unitId); + await page.waitForFunction((targetUnitId) => { + const camp = window.__HEROS_DEBUG__?.camp(); + return camp?.sortieVisible === true && camp?.sortiePrepStep === 'formation' && camp?.sortieFocusedUnitId === targetUnitId; + }, unitId, { timeout: 30000 }); + return; + } + const index = state.sortieRoster.findIndex((unit) => unit.id === unitId); if (index < 0) { throw new Error(`Cannot click sortie unit ${unitId}; unit is missing from roster: ${JSON.stringify(state.sortieRoster)}`); @@ -9824,6 +10006,7 @@ async function clickSortieRosterUnit(page, unitId) { } async function clickReserveTrainingFocus(page, focusId) { + await ensureSortiePrepStep(page, 'loadout'); const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!state?.sortieVisible || !Array.isArray(state.reserveTrainingFocusOptions)) { throw new Error(`Cannot click reserve drill ${focusId}; sortie reserve drill selector is not visible: ${JSON.stringify(state)}`); @@ -9834,8 +10017,10 @@ async function clickReserveTrainingFocus(page, focusId) { throw new Error(`Cannot click reserve drill ${focusId}; focus is missing: ${JSON.stringify(state.reserveTrainingFocusOptions)}`); } - const selectorX = 574; - const buttonWidth = Math.floor((314 - 40) / state.reserveTrainingFocusOptions.length); - await page.mouse.click(selectorX + 12 + index * (buttonWidth + 8) + buttonWidth / 2, 655); + const staged = state.stagedSortiePrep; + const selectorX = staged ? 448 : 574; + const selectorWidth = staged ? 438 : 314; + const buttonWidth = Math.floor((selectorWidth - 40) / state.reserveTrainingFocusOptions.length); + await page.mouse.click(selectorX + 12 + index * (buttonWidth + 8) + buttonWidth / 2, staged ? 548 : 655); await page.waitForTimeout(120); } diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index a8ef6f0..7dc4ff7 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -170,7 +170,15 @@ try { await waitForSortiePrep(page); await page.screenshot({ path: `${screenshotDir}/rc-first-camp-sortie-prep.png`, fullPage: true }); await assertCanvasPainted(page, 'first camp sortie prep'); - await advanceFirstSortiePrepStep(page, 'formation'); + const firstSortieBriefingState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + assert( + firstSortieBriefingState?.stagedSortiePrep === true && + firstSortieBriefingState?.sortiePrepStep === 'briefing' && + firstSortieBriefingState?.sortiePrimaryAction?.kind === 'next' && + firstSortieBriefingState?.sortiePrimaryAction?.nextStep === 'formation', + `Expected the first sortie to open at the staged battlefield briefing: ${JSON.stringify(firstSortieBriefingState)}` + ); + await advanceSortiePrepStep(page, 'formation'); await page.screenshot({ path: `${screenshotDir}/rc-first-camp-sortie-formation.png`, fullPage: true }); await assertCanvasPainted(page, 'first camp sortie formation'); const firstSortieSynergy = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortieSynergyPreview); @@ -182,7 +190,7 @@ try { firstSortieSynergy?.firstPursuitTrinityConfigured === true, `Expected first sortie to preview three bonds, the strongest effect, and trinity: ${JSON.stringify(firstSortieSynergy)}` ); - await advanceFirstSortiePrepStep(page, 'loadout'); + await advanceSortiePrepStep(page, 'loadout'); await page.mouse.click(1116, 656); await advanceUntilBattle(page, 'second-battle-yellow-turban-pursuit'); await page.screenshot({ path: `${screenshotDir}/rc-second-battle-from-first-camp.png`, fullPage: true }); @@ -360,6 +368,42 @@ try { await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-prep.png`, fullPage: true }); await assertCanvasPainted(page, 'mid camp sortie prep'); + const midBriefingState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + assert( + midBriefingState?.stagedSortiePrep === true && + midBriefingState?.firstSortiePrep === false && + midBriefingState?.sortiePrepStep === 'briefing' && + midBriefingState?.sortiePrimaryAction?.kind === 'next' && + midBriefingState?.sortiePrimaryAction?.nextStep === 'formation', + `Expected a mid-campaign battle sortie to open at the staged briefing: ${JSON.stringify(midBriefingState)}` + ); + assert( + midBriefingState?.sortiePortraitRosterView?.enabled === true && + midBriefingState?.sortiePortraitRosterView?.total > 8 && + midBriefingState?.sortiePortraitRosterView?.visibleCount === 8 && + midBriefingState?.sortiePortraitRosterView?.start === 1 && + midBriefingState?.sortiePortraitRosterView?.end === 8, + `Expected the mid-campaign staged formation to expose an eight-card portrait roster window: ${JSON.stringify(midBriefingState?.sortiePortraitRosterView)}` + ); + + await advanceSortiePrepStep(page, 'formation'); + await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-formation.png`, fullPage: true }); + await assertCanvasPainted(page, 'mid camp sortie formation'); + + const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView); + await page.mouse.click(658, 196); + await page.waitForFunction((previousScroll) => { + const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView; + return view?.scroll > previousScroll; + }, midFormationFirstPage?.scroll ?? 0, { timeout: 30000 }); + const midFormationSecondPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView); + assert( + midFormationSecondPage?.start > 1 && midFormationSecondPage?.end > 8, + `Expected the portrait roster next-page control to reveal later officers: ${JSON.stringify(midFormationSecondPage)}` + ); + await page.mouse.click(610, 196); + await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView?.scroll === 0, undefined, { timeout: 30000 }); + const fullRosterCandidateProbe = await page.evaluate(() => { const scene = window.__HEROS_GAME__?.scene.getScene('CampScene'); const state = window.__HEROS_DEBUG__?.camp(); @@ -374,6 +418,7 @@ try { await page.waitForFunction((probe) => { const state = window.__HEROS_DEBUG__?.camp(); return state?.sortieFocusedUnitId === probe.candidateId && + state?.sortiePrepStep === 'formation' && state?.sortieFocusedSynergyPreview?.mode === 'waiting' && state?.selectedSortieUnitIds?.length === probe.selectedBefore.length && !state.selectedSortieUnitIds.includes(probe.candidateId); @@ -393,6 +438,7 @@ try { await page.waitForFunction((probe) => { const state = window.__HEROS_DEBUG__?.camp(); return state?.sortieVisible === true && + state?.sortiePrepStep === 'formation' && state?.selectedSortieUnitIds?.length === probe.selectedBefore.length - 1 && !state.selectedSortieUnitIds.includes(probe.removedUnitId) && state?.sortieFocusedSynergyPreview?.mode === 'add'; @@ -429,8 +475,35 @@ try { const midCampSelectedSortieUnitIds = reserveDoctrineSetup.state?.selectedSortieUnitIds ?? []; const midCampDeploymentPreview = reserveDoctrineSetup.state?.sortieDeploymentPreview ?? []; + const midCampFormationAssignments = reserveDoctrineSetup.state?.sortieFormationAssignments ?? {}; + const midCampItemAssignments = reserveDoctrineSetup.state?.sortieItemAssignments ?? {}; await page.mouse.click(1160, 38); await waitForSortiePrep(page); + const restoredMidBriefingState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + assert( + restoredMidBriefingState?.stagedSortiePrep === true && restoredMidBriefingState?.sortiePrepStep === 'briefing', + `Expected reopening the mid-campaign sortie to restart at briefing: ${JSON.stringify(restoredMidBriefingState)}` + ); + const restoredMidFormationState = await advanceSortiePrepStep(page, 'formation'); + assert( + restoredMidFormationState?.sortiePlan?.formationCoverageComplete === false && + JSON.stringify(restoredMidFormationState?.sortieFormationAssignments) === JSON.stringify(midCampFormationAssignments) && + JSON.stringify(restoredMidFormationState?.sortieItemAssignments) === JSON.stringify(midCampItemAssignments), + `Expected the staged formation screen to preserve role and supply assignments: ${JSON.stringify(restoredMidFormationState)}` + ); + const restoredMidLoadoutState = await advanceSortiePrepStep(page, 'loadout'); + assertSameMembers( + restoredMidLoadoutState?.selectedSortieUnitIds, + midCampSelectedSortieUnitIds, + `Expected briefing, formation, and loadout steps to preserve the selected mid-campaign roster: ${JSON.stringify(restoredMidLoadoutState?.selectedSortieUnitIds)}` + ); + assert( + JSON.stringify(restoredMidLoadoutState?.sortieFormationAssignments) === JSON.stringify(midCampFormationAssignments) && + JSON.stringify(restoredMidLoadoutState?.sortieItemAssignments) === JSON.stringify(midCampItemAssignments), + `Expected loadout navigation to preserve role and supply assignments: ${JSON.stringify(restoredMidLoadoutState)}` + ); + await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-loadout.png`, fullPage: true }); + await assertCanvasPainted(page, 'mid camp sortie loadout'); await page.mouse.click(1116, 656); await advanceUntilBattle(page, midCampNextBattleId); await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-next-battle.png`, fullPage: true }); @@ -489,6 +562,7 @@ try { const finalCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); assert(finalCampState?.campaign?.step === 'sixty-sixth-camp', `Expected final camp save to continue: ${JSON.stringify(finalCampState)}`); assert(finalCampState.sortieHasBattle === false, `Expected final camp to expose no playable sortie target: ${JSON.stringify(finalCampState)}`); + assert(finalCampState.stagedSortiePrep === false, `Expected the non-battle final camp to avoid the three-stage sortie flow: ${JSON.stringify(finalCampState)}`); assert(finalCampState.nextSortieBattleId === null, `Expected final camp to route to ending instead of a battle: ${JSON.stringify(finalCampState)}`); assert( Array.isArray(finalCampState.sortieDeploymentPreview) && finalCampState.sortieDeploymentPreview.length === 0, @@ -675,12 +749,24 @@ async function waitForSortiePrep(page) { }, undefined, { timeout: 30000 }); } -async function advanceFirstSortiePrepStep(page, expectedStep) { +async function advanceSortiePrepStep(page, expectedStep) { await page.mouse.click(1116, 656); await page.waitForFunction((step) => { const camp = window.__HEROS_DEBUG__?.camp?.(); return camp?.sortieVisible === true && camp?.sortiePrepStep === step; }, expectedStep, { timeout: 30000 }); + await page.waitForTimeout(1000); + const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp?.()); + const expectedNextStep = expectedStep === 'formation' ? 'loadout' : null; + assert( + state?.stagedSortiePrep === true && + state?.sortiePrepSteps?.filter((step) => step.active).length === 1 && + state?.sortiePrepSteps?.some((step) => step.id === expectedStep && step.active) && + state?.sortiePrimaryAction?.kind === (expectedNextStep ? 'next' : 'launch') && + state?.sortiePrimaryAction?.nextStep === expectedNextStep, + `Expected staged sortie navigation to reach ${expectedStep}: ${JSON.stringify(state)}` + ); + return state; } async function waitForCampAfterBattleResult(page) { diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 1d31b9c..0d34039 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -18881,7 +18881,12 @@ export class BattleScene extends Phaser.Scene { } private updateObjectiveTracker() { - if (!this.objectiveTrackerText || !this.objectiveTrackerSubText) { + if ( + !this.objectiveTrackerText?.active || + !this.objectiveTrackerSubText?.active || + !this.objectiveTrackerText.frame?.texture || + !this.objectiveTrackerSubText.frame?.texture + ) { return; } diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index e91ab6d..17a5c8e 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -12221,7 +12221,7 @@ export class CampScene extends Phaser.Scene { this.sortieObjects.length === 0 || this.saveSlotObjects.length > 0 || this.saveSlotConfirmObjects.length > 0 || - !this.isFirstSortiePrepSlice() + !this.usesStagedSortiePrep() ) { return; } @@ -12348,7 +12348,8 @@ export class CampScene extends Phaser.Scene { const flow = this.currentSortieFlow(); const isBattleSortie = Boolean(flow.nextBattleId); const isFirstSortieSlice = this.isFirstSortiePrepSlice(flow); - if (isFirstSortieSlice && !wasVisible) { + const usesStagedPrep = this.usesStagedSortiePrep(flow); + if (usesStagedPrep && !wasVisible) { this.sortiePrepStep = 'briefing'; } const checklist = this.sortieChecklist(); @@ -12358,8 +12359,13 @@ export class CampScene extends Phaser.Scene { formation: '큰 초상 카드를 고르고 전열·돌파·후원 역할을 맞바꾸십시오.', loadout: '장비·보급과 필수 조건을 마지막으로 확인하십시오.' }; - const prepSubtitle = isFirstSortieSlice - ? firstSliceSubtitle[this.sortiePrepStep] + const campaignSortieSubtitle: Record = { + briefing: `${this.nextSortieBriefing().title}의 목표·지형·적 위협을 먼저 읽으십시오.`, + formation: '초상 카드로 무장을 고르고 전열·돌파·후원 역할과 공명을 조율하십시오.', + loadout: '선택한 무장의 장비·보급과 필수 출전 조건을 마지막으로 점검하십시오.' + }; + const prepSubtitle = usesStagedPrep + ? (isFirstSortieSlice ? firstSliceSubtitle : campaignSortieSubtitle)[this.sortiePrepStep] : isBattleSortie ? this.sortiePrepSubtitle(checklist) : this.isFinalEpilogueFlow(flow) @@ -12371,7 +12377,7 @@ export class CampScene extends Phaser.Scene { shade.setDepth(depth); shade.setInteractive(); - const panel = this.trackSortie(this.add.rectangle(x, y, width, height, isFirstSortieSlice ? 0x0b1118 : 0x101820, isFirstSortieSlice ? 0.94 : 0.98)); + const panel = this.trackSortie(this.add.rectangle(x, y, width, height, usesStagedPrep ? 0x0b1118 : 0x101820, usesStagedPrep ? 0.96 : 0.98)); panel.setOrigin(0); panel.setDepth(depth + 1); panel.setStrokeStyle(3, palette.gold, 0.92); @@ -12379,20 +12385,20 @@ export class CampScene extends Phaser.Scene { this.renderFirstSortieArtwork(x + 3, y + 3, width - 6, height - 6, depth + 1, 0.13); } - const title = this.trackSortie(this.add.text(x + 34, y + (isFirstSortieSlice ? 22 : 28), prepTitle, this.textStyle(isFirstSortieSlice ? 28 : 30, '#f2e3bf', true))); + const title = this.trackSortie(this.add.text(x + 34, y + (usesStagedPrep ? 22 : 28), prepTitle, this.textStyle(usesStagedPrep ? 28 : 30, '#f2e3bf', true))); title.setDepth(depth + 2); const subtitle = this.trackSortie( this.add.text( x + 34, - y + (isFirstSortieSlice ? 60 : 70), - this.compactText(prepSubtitle, isFirstSortieSlice ? 54 : 72), + y + (usesStagedPrep ? 60 : 70), + this.compactText(prepSubtitle, usesStagedPrep ? 62 : 72), this.textStyle(15, '#d4dce6') ) ); subtitle.setDepth(depth + 2); - this.renderSortieHeaderSummary(x + (isFirstSortieSlice ? 748 : 690), y + 26, isFirstSortieSlice ? 406 : 464, 58, depth + 2); + this.renderSortieHeaderSummary(x + (usesStagedPrep ? 748 : 690), y + 26, usesStagedPrep ? 406 : 464, 58, depth + 2); - if (isFirstSortieSlice) { + if (usesStagedPrep) { this.renderFirstSortieStepNavigation(x + 34, y + 92, width - 68, 42, depth + 2, checklist); const contentX = x + 34; const contentY = y + 150; @@ -12401,7 +12407,11 @@ export class CampScene extends Phaser.Scene { if (this.sortiePrepStep === 'briefing') { this.renderFirstSortieBriefingStep(contentX, contentY, contentWidth, contentHeight, depth + 2); } else if (this.sortiePrepStep === 'formation') { - this.renderFirstSortieFormationStep(contentX, contentY, contentWidth, contentHeight, depth + 2); + if (isFirstSortieSlice) { + this.renderFirstSortieFormationStep(contentX, contentY, contentWidth, contentHeight, depth + 2); + } else { + this.renderCampaignSortieFormationStep(contentX, contentY, contentWidth, contentHeight, depth + 2); + } } else { this.renderFirstSortieLoadoutStep(contentX, contentY, contentWidth, contentHeight, depth + 2, checklist); } @@ -12460,11 +12470,16 @@ export class CampScene extends Phaser.Scene { return flow.afterBattleId === campBattleIds.first && flow.nextBattleId === campBattleIds.second; } + private usesStagedSortiePrep(flow = this.currentSortieFlow()) { + return Boolean(flow.nextBattleId); + } + private setSortiePrepStep(step: SortiePrepStep) { if (this.sortiePrepStep === step) { return; } this.sortiePrepStep = step; + this.sortieRosterScroll = 0; soundDirector.playSelect(); this.showSortiePrep(); } @@ -12531,8 +12546,9 @@ export class CampScene extends Phaser.Scene { const gap = 18; const briefingWidth = 690; const tacticsWidth = width - briefingWidth - gap; + const useFirstSortieArtwork = this.isFirstSortiePrepSlice(); this.renderSortieBriefing(x, y, briefingWidth, 220, depth); - this.renderSortieDeploymentMap(x, y + 238, briefingWidth, height - 238, depth, true); + this.renderSortieDeploymentMap(x, y + 238, briefingWidth, height - 238, depth, useFirstSortieArtwork); const mapLabelBg = this.trackSortie(this.add.rectangle(x + 12, y + 250, 162, 28, 0x070b10, 0.88)); mapLabelBg.setOrigin(0); mapLabelBg.setDepth(depth + 3); @@ -12543,12 +12559,20 @@ export class CampScene extends Phaser.Scene { private renderFirstSortieTacticalNotes(x: number, y: number, width: number, height: number, depth: number) { const scenario = this.nextSortieScenario(); + const firstSortie = this.isFirstSortiePrepSlice(); const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.94)); bg.setOrigin(0); bg.setDepth(depth); bg.setStrokeStyle(1, palette.gold, 0.58); this.trackSortie(this.add.text(x + 20, y + 16, '전술 핵심', this.textStyle(20, '#f2e3bf', true))).setDepth(depth + 1); - this.trackSortie(this.add.text(x + 20, y + 44, '세 형제의 역할이 갈리는 첫 추격전입니다.', this.textStyle(12, '#9fb0bf'))).setDepth(depth + 1); + this.trackSortie( + this.add.text( + x + 20, + y + 44, + firstSortie ? '세 형제의 역할이 갈리는 첫 추격전입니다.' : '목표와 지형에 맞춰 출진 교리를 세우십시오.', + this.textStyle(12, '#9fb0bf') + ) + ).setDepth(depth + 1); if (!scenario) { return; } @@ -12589,7 +12613,9 @@ export class CampScene extends Phaser.Scene { command.setStrokeStyle(1, palette.gold, 0.6); this.trackSortie(this.add.text(x + 34, commandY + 12, '지휘관 판단', this.textStyle(14, '#ffdf7b', true))).setDepth(depth + 2); this.trackSortie( - this.add.text(x + 34, commandY + 36, scenario.tacticalGuide?.summary ?? scenario.openingObjectiveLines[1] ?? '세 형제가 서로 다른 길을 맡아 적의 전열을 흔드십시오.', { + this.add.text(x + 34, commandY + 36, scenario.tacticalGuide?.summary ?? scenario.openingObjectiveLines[1] ?? (firstSortie + ? '세 형제가 서로 다른 길을 맡아 적의 전열을 흔드십시오.' + : '전열·돌파·후원의 균형으로 적의 주력을 나누십시오.'), { ...this.textStyle(13, '#d4dce6'), wordWrap: { width: width - 68, useAdvancedWrap: true }, lineSpacing: 3 @@ -12623,6 +12649,193 @@ export class CampScene extends Phaser.Scene { this.renderFirstSortieSynergyPanel(x + rosterWidth + gap, y + 310, planWidth, height - 310, depth); } + private renderCampaignSortieFormationStep(x: number, y: number, width: number, height: number, depth: number) { + const gap = 18; + const rosterWidth = 610; + const planWidth = width - rosterWidth - gap; + this.renderCampaignSortiePortraitRoster(x, y, rosterWidth, height, depth); + this.renderSortieFormationPlan(x + rosterWidth + gap, y, planWidth, 286, depth); + this.renderCampaignSortieComparisonPanel(x + rosterWidth + gap, y + 304, planWidth, height - 304, depth); + } + + private renderCampaignSortiePortraitRoster(x: number, y: number, width: number, height: number, depth: number) { + const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.96)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, palette.blue, 0.58); + + const rosterUnits = this.sortieRosterUnits(); + const displayUnits = this.sortieRosterDisplayUnits(rosterUnits); + const visibleCount = 8; + const maxScroll = Math.max(0, displayUnits.length - visibleCount); + this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll); + const visibleUnits = displayUnits.slice(this.sortieRosterScroll, this.sortieRosterScroll + visibleCount); + const selectedCount = this.selectedSortieUnitIds.length; + const maxUnits = this.sortieMaxUnits(); + const rangeEnd = this.sortieRosterScroll + visibleUnits.length; + this.trackSortie(this.add.text(x + 18, y + 13, `무장 선택 · 출전 ${selectedCount}/${maxUnits}`, this.textStyle(19, '#f2e3bf', true))).setDepth(depth + 1); + const rangeText = this.trackSortie( + this.add.text( + x + width - 116, + y + 18, + `${displayUnits.length > 0 ? this.sortieRosterScroll + 1 : 0}-${rangeEnd}/${displayUnits.length}`, + this.textStyle(11, '#9fb0bf', true) + ) + ); + rangeText.setOrigin(1, 0); + rangeText.setDepth(depth + 1); + this.renderFirstSortieInlineButton('‹', x + width - 100, y + 9, 40, 30, this.sortieRosterScroll > 0, false, () => { + soundDirector.playSelect(); + this.updateSortieRosterScroll(this.sortieRosterScroll - visibleCount, maxScroll); + }, depth + 1); + this.renderFirstSortieInlineButton('›', x + width - 52, y + 9, 40, 30, this.sortieRosterScroll < maxScroll, false, () => { + soundDirector.playSelect(); + this.updateSortieRosterScroll(this.sortieRosterScroll + visibleCount, maxScroll); + }, depth + 1); + + const handleRosterWheel = ( + _pointer: Phaser.Input.Pointer, + _deltaX: number, + deltaY: number, + _deltaZ: number, + event?: { stopPropagation: () => void } + ) => { + event?.stopPropagation(); + const direction = Math.sign(deltaY); + if (direction === 0 || maxScroll <= 0) { + return; + } + this.updateSortieRosterScroll(this.sortieRosterScroll + direction * 2, maxScroll); + }; + if (maxScroll > 0) { + bg.setInteractive({ useHandCursor: false }); + bg.on('wheel', handleRosterWheel); + } + + const columnGap = 10; + const rowGap = 8; + const cardWidth = Math.floor((width - 36 - columnGap) / 2); + const cardTop = y + 50; + const cardHeight = Math.floor((height - 64 - rowGap * 3) / 4); + visibleUnits.forEach((unit, index) => { + const column = index % 2; + const rowIndex = Math.floor(index / 2); + const cardX = x + 18 + column * (cardWidth + columnGap); + const cardY = cardTop + rowIndex * (cardHeight + rowGap); + const selected = this.isSortieSelected(unit.id); + const focused = this.sortieFocusedUnitId === unit.id; + const required = this.isRequiredSortieUnit(unit.id); + const recommendation = this.sortieRecommendation(unit.id); + const availability = this.sortieUnitAvailability(unit); + const role = this.sortieFormationRole(unit); + const preview = this.sortieUnitSynergyPreview(unit); + const fill = !availability.available ? 0x11151a : selected ? 0x172a22 : recommendation ? 0x241f17 : 0x151b24; + const card = this.trackSortie(this.add.rectangle(cardX, cardY, cardWidth, cardHeight, fill, !availability.available ? 0.62 : 0.96)); + card.setOrigin(0); + card.setDepth(depth + 1); + card.setStrokeStyle( + focused ? 2 : 1, + focused ? palette.gold : !availability.available ? 0x53606c : selected ? palette.green : recommendation ? palette.gold : 0x53606c, + focused ? 0.96 : selected ? 0.64 : recommendation ? 0.48 : 0.34 + ); + card.setInteractive({ useHandCursor: true }); + card.on('wheel', handleRosterWheel); + card.on('pointerover', () => card.setFillStyle(!availability.available ? 0x15191e : selected ? 0x20362a : 0x1d2731, 0.98)); + card.on('pointerout', () => card.setFillStyle(fill, !availability.available ? 0.62 : 0.96)); + card.on('pointerdown', () => { + this.sortieFocusedUnitId = unit.id; + soundDirector.playSelect(); + if (!availability.available) { + this.showCampNotice(`${unit.name}: ${availability.reason}`); + } + this.showSortiePrep(); + }); + + this.renderFirstSortiePortrait(cardX + 39, cardY + cardHeight / 2, 56, unit, depth + 2, availability.available ? 1 : 0.42, false); + const statusLabel = required ? '필수' : selected ? '출전' : recommendation ? '추천' : '대기'; + const statusColor = required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf'; + const status = this.trackSortie(this.add.text(cardX + cardWidth - 10, cardY + 8, statusLabel, this.textStyle(9, statusColor, true))); + status.setOrigin(1, 0); + status.setDepth(depth + 2); + this.trackSortie(this.add.text(cardX + 75, cardY + 8, `${unit.name} Lv${unit.level}`, this.textStyle(14, availability.available ? '#f2e3bf' : '#77818c', true))).setDepth(depth + 2); + this.trackSortie(this.add.text(cardX + 75, cardY + 28, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(9, availability.available ? '#c8d2dd' : '#68727e', true))).setDepth(depth + 2); + const previewColor = preview.mode === 'remove' + ? '#ff9d7d' + : preview.mode === 'add' || preview.mode === 'current' + ? '#a8ffd0' + : preview.mode === 'waiting' + ? '#ffdf7b' + : '#87919c'; + this.trackSortie( + this.add.text( + cardX + 75, + cardY + 45, + this.compactText(`${this.sortieFormationRoleLabel(role)} · ${preview.compactLabel}`, 28), + this.textStyle(9, availability.available ? previewColor : '#68727e', selected || Boolean(recommendation)) + ) + ).setDepth(depth + 2); + + const canToggle = availability.available && !(selected && required); + this.renderFirstSortieInlineButton( + required ? '필수 출전' : selected ? '출전 해제' : '출전 등록', + cardX + cardWidth - 80, + cardY + cardHeight - 25, + 70, + 20, + canToggle, + selected, + () => { + this.sortieFocusedUnitId = unit.id; + this.toggleSortieUnit(unit.id); + }, + depth + 3 + ); + }); + } + + private renderCampaignSortieComparisonPanel(x: number, y: number, width: number, height: number, depth: number) { + const focused = this.sortieFocusedUnit(); + const synergy = this.sortieSynergySnapshot(); + const plan = this.sortiePlanSummary(); + const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.96)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, synergy.coveredRoleCount === 3 ? palette.green : palette.gold, 0.58); + this.trackSortie(this.add.text(x + 18, y + 12, focused ? `편성 변화 · ${focused.name}` : '편성 변화', this.textStyle(16, '#f2e3bf', true))).setDepth(depth + 1); + this.trackSortie( + this.add.text( + x + width - 18, + y + 14, + `공명 ${synergy.activeBondCount} · 역할 ${synergy.coveredRoleCount}/3 · 지형 ${synergy.terrainGrade}`, + this.textStyle(10, '#d8b15f', true) + ) + ).setOrigin(1, 0).setDepth(depth + 1); + + if (!focused) { + this.trackSortie(this.add.text(x + 18, y + 44, '왼쪽 초상 카드를 선택하면 합류·해제 전후가 표시됩니다.', this.textStyle(12, '#9fb0bf'))).setDepth(depth + 1); + return; + } + const preview = this.sortieUnitSynergyPreview(focused); + const previewColor = preview.mode === 'remove' + ? '#ff9d7d' + : preview.mode === 'add' || preview.mode === 'current' + ? '#a8ffd0' + : preview.mode === 'waiting' + ? '#ffdf7b' + : '#87919c'; + this.trackSortie(this.add.text(x + 18, y + 39, this.compactText(preview.headline, 54), this.textStyle(12, previewColor, true))).setDepth(depth + 1); + this.trackSortie(this.add.text(x + 18, y + 62, this.compactText(preview.detail, 66), this.textStyle(10, '#c8d2dd'))).setDepth(depth + 1); + const feedback = this.sortiePlanFeedback || plan.warningLine; + this.trackSortie( + this.add.text( + x + 18, + y + height - 27, + this.compactText(feedback, 64), + this.textStyle(10, this.sortiePlanFeedback ? '#a8ffd0' : plan.warnings.length > 0 ? '#ffdf7b' : '#9fb0bf', true) + ) + ).setDepth(depth + 1); + } + private renderFirstSortieRoleDiagram(x: number, y: number, width: number, height: number, depth: number) { const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.94)); bg.setOrigin(0); @@ -12742,12 +12955,20 @@ export class CampScene extends Phaser.Scene { }); } - private renderFirstSortiePortrait(x: number, y: number, size: number, unit: UnitData, depth: number, alpha = 1) { + private renderFirstSortiePortrait( + x: number, + y: number, + size: number, + unit: UnitData, + depth: number, + alpha = 1, + preferPeriodPortrait = true + ) { const frame = this.trackSortie(this.add.rectangle(x, y, size + 8, size + 8, 0x090e14, 0.98)); frame.setDepth(depth); frame.setStrokeStyle(2, palette.gold, 0.58); const portraitKey = portraitByUnitId[unit.id]; - const periodPortraitTexture = portraitKey + const periodPortraitTexture = portraitKey && preferPeriodPortrait ? portraitAssetEntriesForKey(portraitKey).find((entry) => entry.textureKey.endsWith('-yellow-turban'))?.textureKey : undefined; const portraitTexture = periodPortraitTexture && this.textures.exists(periodPortraitTexture) @@ -12853,17 +13074,21 @@ export class CampScene extends Phaser.Scene { const checksWidth = width - rosterWidth - focusWidth - gap * 2; this.renderSortieUnitSummary(x, y, rosterWidth, height, depth, 'focus'); this.renderSortieFocusPanel(x + rosterWidth + gap, y, focusWidth, 320, depth); - this.renderSortieRewardHint(x + rosterWidth + gap, y + 338, focusWidth, height - 338, depth); - const focused = this.sortieFocusedUnit(); - if (focused) { - this.trackSortie( - this.add.text( - x + rosterWidth + gap + 16, - y + 397, - this.compactText(this.sortieBondLine(focused), 52), - this.textStyle(12, '#a8ffd0', true) - ) - ).setDepth(depth + 2); + if (this.isFirstSortiePrepSlice()) { + this.renderSortieRewardHint(x + rosterWidth + gap, y + 338, focusWidth, height - 338, depth); + const focused = this.sortieFocusedUnit(); + if (focused) { + this.trackSortie( + this.add.text( + x + rosterWidth + gap + 16, + y + 397, + this.compactText(this.sortieBondLine(focused), 52), + this.textStyle(12, '#a8ffd0', true) + ) + ).setDepth(depth + 2); + } + } else { + this.renderSortieReserveTraining(x + rosterWidth + gap, y + 338, focusWidth, height - 338, depth); } this.renderFirstSortieFinalChecks(x + rosterWidth + focusWidth + gap * 2, y, checksWidth, height, depth, checklist); } @@ -16306,6 +16531,7 @@ export class CampScene extends Phaser.Scene { } this.selectedSortieUnitIds = this.normalizedSortieUnitIds(Array.from(selected)); + this.sortieRosterScroll = 0; const selectionLabel = hasBattle ? '출전' : '동행'; const synergyResultLine = synergyPreview.headline.replace(/^(합류|해제) 시 /, ''); this.sortiePlanFeedback = `${unit.name} ${selected.has(unitId) ? `${selectionLabel} 추가` : `${selectionLabel} 해제`} · ${synergyResultLine}`; @@ -16570,12 +16796,41 @@ export class CampScene extends Phaser.Scene { const reserveTrainingFocus = this.reserveTrainingFocusDefinition(); const reserveTrainingUnits = this.reserveTrainingPreviewUnits(); const focusedSortieUnit = this.sortieFocusedUnit(); + const stagedSortiePrep = this.usesStagedSortiePrep(); + const sortieStepIndex = firstSortiePrepSteps.findIndex((step) => step.id === this.sortiePrepStep); + const nextSortiePrepStep = stagedSortiePrep ? firstSortiePrepSteps[sortieStepIndex + 1]?.id ?? null : null; + const portraitRosterUnits = this.sortieRosterDisplayUnits(this.sortieRosterUnits()); + const portraitRosterVisibleCount = 8; + const portraitRosterMaxScroll = Math.max(0, portraitRosterUnits.length - portraitRosterVisibleCount); + const portraitRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, portraitRosterMaxScroll); return { scene: this.scene.key, activeTab: this.activeTab, sortieVisible: this.sortieObjects.length > 0, sortiePrepStep: this.sortiePrepStep, - stagedSortiePrep: this.isFirstSortiePrepSlice(), + stagedSortiePrep, + firstSortiePrep: this.isFirstSortiePrepSlice(), + sortiePrepSteps: firstSortiePrepSteps.map((step) => ({ + id: step.id, + active: step.id === this.sortiePrepStep, + complete: this.firstSortieStepComplete(step.id, sortieChecklist) + })), + sortiePrimaryAction: stagedSortiePrep + ? { + kind: nextSortiePrepStep ? 'next' : 'launch', + nextStep: nextSortiePrepStep, + label: nextSortiePrepStep === 'formation' ? '편성하기' : nextSortiePrepStep === 'loadout' ? '장비·보급' : '출진' + } + : null, + sortiePortraitRosterView: { + enabled: stagedSortiePrep && !this.isFirstSortiePrepSlice(), + total: portraitRosterUnits.length, + visibleCount: Math.min(portraitRosterVisibleCount, portraitRosterUnits.length), + start: portraitRosterUnits.length > 0 ? portraitRosterScroll + 1 : 0, + end: portraitRosterScroll + Math.min(portraitRosterVisibleCount, portraitRosterUnits.length - portraitRosterScroll), + maxScroll: portraitRosterMaxScroll, + scroll: portraitRosterScroll + }, sortieHasBattle: Boolean(sortieScenario), nextSortieBattleId: sortieScenario?.id ?? null, nextSortieBattleTitle: sortieScenario?.title ?? null, @@ -16661,6 +16916,8 @@ export class CampScene extends Phaser.Scene { inventory: this.campaign.inventory, selectedSortieUnitIds: this.campaign.selectedSortieUnitIds, sortieFormationAssignments: this.campaign.sortieFormationAssignments, + sortieItemAssignments: this.campaign.sortieItemAssignments, + activeSaveSlot: this.campaign.activeSaveSlot, reserveTrainingAssignments: this.campaign.reserveTrainingAssignments, reserveTrainingFocus: this.campaign.reserveTrainingFocus, roster: this.campaign.roster.map((unit) => ({