import { spawn } from 'node:child_process'; import { chromium } from 'playwright'; const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/'; let serverProcess; let browser; try { serverProcess = await ensureLocalServer(targetUrl); browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }); await page.evaluate(() => window.localStorage.clear()); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => document.querySelector('canvas') !== null, undefined, { timeout: 90000 }); await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined, undefined, { timeout: 90000 }); await page.waitForFunction(() => window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 }); await page.waitForTimeout(500); const debugBeforeBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []); if (!debugBeforeBattle.includes('TitleScene')) { throw new Error(`Expected TitleScene before starting. Active scenes: ${debugBeforeBattle.join(', ')}`); } await page.mouse.click(962, 240); await page.waitForTimeout(250); for (let i = 0; i < 20; i += 1) { const isBattleActive = await page.evaluate(() => { const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? []; return activeScenes.some((scene) => scene.scene.key === 'BattleScene'); }); if (isBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(220); } await page.waitForFunction(() => { const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? []; return activeScenes.some((scene) => scene.scene.key === 'BattleScene'); }); await page.keyboard.press('F9'); await page.waitForTimeout(100); await page.screenshot({ path: 'dist/verification-battle.png', fullPage: true }); const result = await page.evaluate(() => { const canvas = document.querySelector('canvas'); const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; const battleState = window.__HEROS_DEBUG__?.battle(); return { activeScenes, battleState, canvasWidth: canvas?.width ?? 0, canvasHeight: canvas?.height ?? 0 }; }); if (result.canvasWidth !== 1280 || result.canvasHeight !== 720) { throw new Error(`Unexpected canvas size: ${result.canvasWidth}x${result.canvasHeight}`); } if (!result.activeScenes.includes('BattleScene')) { throw new Error(`BattleScene was not active. Active scenes: ${result.activeScenes.join(', ')}`); } if (!result.battleState || result.battleState.scene !== 'BattleScene') { throw new Error(`Debug battle state was not available: ${JSON.stringify(result.battleState)}`); } if (result.battleState.battleId !== 'first-battle-zhuo-commandery') { throw new Error(`Expected first battle scenario id: ${JSON.stringify(result.battleState)}`); } if (result.battleState.victoryConditionLabel !== '두령 한석 격파' || result.battleState.defeatConditionLabel !== '유비 퇴각') { throw new Error(`Expected first battle objective labels: ${JSON.stringify(result.battleState)}`); } const openingLeaderObjective = result.battleState.objectives.find((objective) => objective.id === 'leader'); if (!openingLeaderObjective || openingLeaderObjective.status !== 'active' || openingLeaderObjective.achieved) { throw new Error(`Expected live leader objective to start active: ${JSON.stringify(result.battleState.objectives)}`); } const actionTexturesLoaded = await page.evaluate(() => { const textures = window.__HEROS_GAME__?.textures; return [ 'unit-liu-bei-actions', 'unit-guan-yu-actions', 'unit-zhang-fei-actions', 'unit-rebel-actions', 'unit-rebel-archer-actions', 'unit-rebel-cavalry-actions', 'unit-rebel-leader-actions' ].every((key) => textures?.exists(key)); }); if (!actionTexturesLoaded) { throw new Error('Expected all unit action textures to be loaded.'); } const movementBlockingState = await page.evaluate(() => { const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene'); const state = window.__HEROS_DEBUG__?.battle(); const liuBei = state?.units?.find((unit) => unit.id === 'liu-bei'); const guanYu = state?.units?.find((unit) => unit.id === 'guan-yu'); const enemy = state?.units?.find((unit) => unit.faction === 'enemy'); if (!scene || !liuBei || !guanYu || !enemy) { throw new Error('Expected battle scene and movement test units.'); } return { allyBlocksPath: scene.isMovementBlocked(liuBei, guanYu.x, guanYu.y), enemyBlocksPath: scene.isMovementBlocked(liuBei, enemy.x, enemy.y), allyStopTileReachable: scene.reachableTiles(liuBei).some((tile) => tile.x === guanYu.x && tile.y === guanYu.y) }; }); if ( movementBlockingState.allyBlocksPath || !movementBlockingState.enemyBlocksPath || movementBlockingState.allyStopTileReachable ) { throw new Error(`Expected allies to be passable path blockers only as stop tiles, and enemies to block movement: ${JSON.stringify(movementBlockingState)}`); } const liuBeiScreen = await page.evaluate(() => { const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene'); const state = window.__HEROS_DEBUG__?.battle(); const liuBei = state?.units?.find((unit) => unit.id === 'liu-bei'); if (!scene || !liuBei) { throw new Error('Expected Liu Bei and battle scene for stand-still command test.'); } return { x: scene.tileCenterX(liuBei.x), y: scene.tileCenterY(liuBei.y) }; }); await page.mouse.click(liuBeiScreen.x, liuBeiScreen.y); await page.waitForTimeout(140); await page.mouse.click(liuBeiScreen.x, liuBeiScreen.y); await page.waitForTimeout(180); const standStillCommandState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); if ( standStillCommandState?.phase !== 'command' || standStillCommandState.selectedUnitId !== 'liu-bei' || standStillCommandState.pendingMove?.unitId !== 'liu-bei' || standStillCommandState.pendingMove.from.x !== standStillCommandState.pendingMove.to.x || standStillCommandState.pendingMove.from.y !== standStillCommandState.pendingMove.to.y ) { throw new Error(`Expected Liu Bei to open commands without moving: ${JSON.stringify(standStillCommandState)}`); } await page.evaluate(() => { const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene'); if (!scene) { return; } scene.selectedUnit = undefined; scene.pendingMove = undefined; scene.targetingAction = undefined; scene.selectedUsable = undefined; scene.phase = 'idle'; scene.clearMarkers(); scene.hideCommandMenu(); scene.hideMapMenu(); }); await page.waitForTimeout(700); await page.mouse.click(260, 300, { button: 'right' }); await page.waitForTimeout(120); await page.mouse.click(182, 196); await page.waitForTimeout(160); const threatState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); if (!threatState?.threatMarkerCount || threatState.threatMarkerCount <= 0) { throw new Error(`Expected enemy threat markers from map menu: ${JSON.stringify(threatState)}`); } await page.mouse.click(260, 300, { button: 'right' }); await page.waitForTimeout(120); await page.mouse.click(182, 230); await page.waitForTimeout(120); await page.mouse.click(400, 213); await page.waitForTimeout(180); const battleSlotSave = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:battle:first-battle-zhuo-commandery:slot-1'); return raw ? JSON.parse(raw) : undefined; }); if (!battleSlotSave || battleSlotSave.battleId !== 'first-battle-zhuo-commandery') { throw new Error(`Expected slot 1 battle save from save UI: ${JSON.stringify(battleSlotSave)}`); } await page.mouse.click(260, 300, { button: 'right' }); await page.waitForTimeout(120); await page.mouse.click(182, 264); await page.waitForTimeout(120); await page.mouse.click(400, 213); await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.scene === 'BattleScene'); const cameraBeforeScroll = result.battleState.camera; if ( !cameraBeforeScroll || cameraBeforeScroll.mapWidth <= cameraBeforeScroll.visibleColumns || cameraBeforeScroll.mapHeight <= cameraBeforeScroll.visibleRows ) { throw new Error(`Expected a scrollable battle map: ${JSON.stringify(cameraBeforeScroll)}`); } await page.mouse.move(872, 360); await page.waitForTimeout(520); const cameraAfterEdgeScroll = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.camera); if (!cameraAfterEdgeScroll || cameraAfterEdgeScroll.x <= cameraBeforeScroll.x) { throw new Error(`Expected edge scrolling to move camera right: ${JSON.stringify({ cameraBeforeScroll, cameraAfterEdgeScroll })}`); } await page.mouse.click(1138, 553); await page.waitForTimeout(160); const cameraAfterMiniMapClick = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.camera); if ( !cameraAfterMiniMapClick || cameraAfterMiniMapClick.x < cameraAfterMiniMapClick.mapWidth - cameraAfterMiniMapClick.visibleColumns || cameraAfterMiniMapClick.y !== 0 ) { throw new Error(`Expected minimap click to move camera to the northeast: ${JSON.stringify(cameraAfterMiniMapClick)}`); } const readyUnits = result.battleState.units.filter((unit) => !unit.acted); if (!readyUnits.some((unit) => unit.animating && unit.animationKey?.includes('-idle-'))) { throw new Error(`Expected ready units to loop idle frames: ${JSON.stringify(readyUnits)}`); } await page.keyboard.press('F9'); await page.waitForTimeout(100); await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); const victoryState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const livingEnemiesAfterVictory = victoryState.units.filter((unit) => unit.faction === 'enemy' && unit.hp > 0); if (livingEnemiesAfterVictory.length > 0) { throw new Error(`Expected no living enemies after forced victory: ${JSON.stringify(livingEnemiesAfterVictory)}`); } const victoryLeaderObjective = victoryState.objectives.find((objective) => objective.id === 'leader'); if (!victoryLeaderObjective || victoryLeaderObjective.status !== 'done' || !victoryLeaderObjective.achieved) { throw new Error(`Expected leader objective to resolve after victory: ${JSON.stringify(victoryState.objectives)}`); } await page.screenshot({ path: 'dist/verification-battle-result-victory.png', fullPage: true }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); await page.screenshot({ path: 'dist/verification-camp.png', fullPage: true }); const campState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!campState || campState.scene !== 'CampScene' || campState.report?.rewardGold <= 0) { throw new Error(`Expected CampScene report after victory: ${JSON.stringify(campState)}`); } if ( campState.campBattleId !== 'first-battle-zhuo-commandery' || campState.availableDialogueIds?.length !== 3 || !campState.availableDialogueIds.every((id) => id.endsWith('first-battle')) ) { throw new Error(`Expected first camp to expose first-battle dialogue set: ${JSON.stringify(campState)}`); } await page.evaluate(() => { const campScene = window.__HEROS_GAME__?.scene.getScene('CampScene'); const liuBei = campScene?.campaign?.roster?.find((unit) => unit.id === 'liu-bei'); if (!liuBei) { throw new Error('Expected Liu Bei in camp roster before supply verification.'); } liuBei.hp = Math.max(1, liuBei.maxHp - 10); campScene.report?.units?.forEach((unit) => { if (unit.id === 'liu-bei') { unit.hp = liuBei.hp; } }); campScene.render(); }); await page.mouse.click(888, 38); await page.waitForTimeout(160); await page.screenshot({ path: 'dist/verification-camp-supplies.png', fullPage: true }); await page.mouse.click(1120, 245); await page.waitForTimeout(260); const campStateAfterSupply = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); const liuBeiAfterSupply = campStateAfterSupply?.campaign?.roster?.find((unit) => unit.id === 'liu-bei'); if (!liuBeiAfterSupply || liuBeiAfterSupply.hp !== liuBeiAfterSupply.maxHp) { throw new Error(`Expected bean supply to recover Liu Bei: ${JSON.stringify(campStateAfterSupply)}`); } if ((campStateAfterSupply.campaign?.inventory?.['콩'] ?? 0) !== 0) { throw new Error(`Expected bean supply to be consumed: ${JSON.stringify(campStateAfterSupply.campaign?.inventory)}`); } const goldBeforeMerchant = campStateAfterSupply.campaign?.gold ?? 0; await page.mouse.click(702, 386); await page.waitForTimeout(220); const campStateAfterMerchant = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( (campStateAfterMerchant.campaign?.inventory?.['콩'] ?? 0) !== 1 || campStateAfterMerchant.campaign?.gold !== goldBeforeMerchant - 40 ) { throw new Error(`Expected merchant purchase to add bean and spend gold: ${JSON.stringify({ goldBeforeMerchant, campStateAfterMerchant })}`); } await page.mouse.click(732, 38); await page.waitForTimeout(120); await page.mouse.click(974, 482); await page.waitForTimeout(260); const campStateAfterDialogue = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!campStateAfterDialogue.report?.completedCampDialogues?.length || campStateAfterDialogue.completedAvailableDialogues?.length !== 1) { throw new Error(`Expected camp dialogue to award bond exp: ${JSON.stringify(campStateAfterDialogue)}`); } const campaignSaveAfterDialogue = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterDialogue || campaignSaveAfterDialogue.step !== 'first-camp' || campaignSaveAfterDialogue.latestBattleId !== 'first-battle-zhuo-commandery' || !campaignSaveAfterDialogue.battleHistory?.['first-battle-zhuo-commandery'] || !campaignSaveAfterDialogue.completedCampDialogues?.length || Object.keys(campaignSaveAfterDialogue.inventory ?? {}).length === 0 ) { throw new Error(`Expected campaign save to persist camp progress: ${JSON.stringify(campaignSaveAfterDialogue)}`); } const campaignSlotAfterDialogue = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state:slot-1'); return raw ? JSON.parse(raw) : undefined; }); if (!campaignSlotAfterDialogue || campaignSlotAfterDialogue.latestBattleId !== 'first-battle-zhuo-commandery') { throw new Error(`Expected campaign slot 1 to persist progress: ${JSON.stringify(campaignSlotAfterDialogue)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(180); const campStateWithSortiePrep = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!campStateWithSortiePrep?.sortieVisible) { throw new Error(`Expected sortie preparation overlay from next story button: ${JSON.stringify(campStateWithSortiePrep)}`); } 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); const campStateAfterSortieClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (campStateAfterSortieClose?.sortieVisible) { throw new Error(`Expected sortie preparation overlay to close: ${JSON.stringify(campStateAfterSortieClose)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(120); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); for (let i = 0; i < 24; i += 1) { const isSecondBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'second-battle-yellow-turban-pursuit'; }); if (isSecondBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-second-battle.png', fullPage: true }); const secondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const secondEnemies = secondBattleState.units.filter((unit) => unit.faction === 'enemy'); const secondEnemyBehaviors = new Set(secondEnemies.map((unit) => unit.ai)); if ( secondBattleState.camera?.mapWidth !== 24 || secondBattleState.camera?.mapHeight !== 20 || secondEnemies.length < 10 || !secondEnemyBehaviors.has('aggressive') || !secondEnemyBehaviors.has('guard') || !secondEnemyBehaviors.has('hold') ) { throw new Error(`Expected expanded second battle map and mixed enemy AI: ${JSON.stringify(secondBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const campaignSaveAfterSecondBattle = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterSecondBattle || campaignSaveAfterSecondBattle.step !== 'second-camp' || campaignSaveAfterSecondBattle.latestBattleId !== 'second-battle-yellow-turban-pursuit' || !campaignSaveAfterSecondBattle.battleHistory?.['second-battle-yellow-turban-pursuit'] || !campaignSaveAfterSecondBattle.completedCampDialogues?.length ) { throw new Error(`Expected campaign save to persist second battle victory: ${JSON.stringify(campaignSaveAfterSecondBattle)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const secondCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!secondCampSortieState?.sortieVisible) { throw new Error(`Expected second camp sortie preparation overlay: ${JSON.stringify(secondCampSortieState)}`); } await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); for (let i = 0; i < 24; i += 1) { const isThirdBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road'; }); if (isThirdBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-third-battle.png', fullPage: true }); const thirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirdEnemies = thirdBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirdEnemyBehaviors = new Set(thirdEnemies.map((unit) => unit.ai)); if ( thirdBattleState.camera?.mapWidth !== 24 || thirdBattleState.camera?.mapHeight !== 20 || thirdBattleState.victoryConditionLabel !== '전령 마원 격파' || thirdEnemies.length < 10 || !thirdEnemyBehaviors.has('aggressive') || !thirdEnemyBehaviors.has('guard') || !thirdEnemyBehaviors.has('hold') ) { throw new Error(`Expected third battle map, objective, and mixed enemy AI: ${JSON.stringify(thirdBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const campaignSaveAfterThirdBattle = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterThirdBattle || campaignSaveAfterThirdBattle.step !== 'third-camp' || campaignSaveAfterThirdBattle.latestBattleId !== 'third-battle-guangzong-road' || !campaignSaveAfterThirdBattle.battleHistory?.['third-battle-guangzong-road'] ) { throw new Error(`Expected campaign save to persist third battle victory: ${JSON.stringify(campaignSaveAfterThirdBattle)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const thirdCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!thirdCampSortieState?.sortieVisible) { throw new Error(`Expected third camp sortie preparation overlay: ${JSON.stringify(thirdCampSortieState)}`); } await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); for (let i = 0; i < 26; i += 1) { const isFourthBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'fourth-battle-guangzong-camp'; }); if (isFourthBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-fourth-battle.png', fullPage: true }); const fourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const fourthEnemies = fourthBattleState.units.filter((unit) => unit.faction === 'enemy'); const fourthEnemyBehaviors = new Set(fourthEnemies.map((unit) => unit.ai)); if ( fourthBattleState.camera?.mapWidth !== 24 || fourthBattleState.camera?.mapHeight !== 20 || fourthBattleState.victoryConditionLabel !== '장각 격파' || fourthEnemies.length < 12 || !fourthEnemyBehaviors.has('aggressive') || !fourthEnemyBehaviors.has('guard') || !fourthEnemyBehaviors.has('hold') ) { throw new Error(`Expected fourth battle map, objective, and mixed enemy AI: ${JSON.stringify(fourthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const campaignSaveAfterFourthBattle = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterFourthBattle || campaignSaveAfterFourthBattle.step !== 'fourth-camp' || campaignSaveAfterFourthBattle.latestBattleId !== 'fourth-battle-guangzong-camp' || !campaignSaveAfterFourthBattle.battleHistory?.['fourth-battle-guangzong-camp'] ) { throw new Error(`Expected campaign save to persist fourth battle victory: ${JSON.stringify(campaignSaveAfterFourthBattle)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const fourthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!fourthCampSortieState?.sortieVisible) { throw new Error(`Expected fourth camp sortie preparation overlay: ${JSON.stringify(fourthCampSortieState)}`); } await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); for (let i = 0; i < 26; i += 1) { const isFifthBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'fifth-battle-sishui-vanguard'; }); if (isFifthBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-fifth-battle.png', fullPage: true }); const fifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const fifthEnemies = fifthBattleState.units.filter((unit) => unit.faction === 'enemy'); const fifthEnemyBehaviors = new Set(fifthEnemies.map((unit) => unit.ai)); if ( fifthBattleState.camera?.mapWidth !== 24 || fifthBattleState.camera?.mapHeight !== 20 || fifthBattleState.victoryConditionLabel !== '호진 격파' || fifthEnemies.length < 10 || !fifthEnemyBehaviors.has('aggressive') || !fifthEnemyBehaviors.has('guard') || !fifthEnemyBehaviors.has('hold') ) { throw new Error(`Expected fifth battle map, objective, and mixed enemy AI: ${JSON.stringify(fifthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const campaignSaveAfterFifthBattle = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterFifthBattle || campaignSaveAfterFifthBattle.step !== 'fifth-camp' || campaignSaveAfterFifthBattle.latestBattleId !== 'fifth-battle-sishui-vanguard' || !campaignSaveAfterFifthBattle.battleHistory?.['fifth-battle-sishui-vanguard'] ) { throw new Error(`Expected campaign save to persist fifth battle victory: ${JSON.stringify(campaignSaveAfterFifthBattle)}`); } const fifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( fifthCampState?.campBattleId !== 'fifth-battle-sishui-vanguard' || fifthCampState.campTitle !== '공손찬 진영 군영' || fifthCampState.availableDialogueIds?.length !== 3 || !fifthCampState.availableDialogueIds.every((id) => id.endsWith('sishui')) ) { throw new Error(`Expected fifth camp to expose Gongsun Zan/Sishui dialogue set: ${JSON.stringify(fifthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const fifthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!fifthCampSortieState?.sortieVisible) { throw new Error(`Expected fifth camp sortie preparation overlay: ${JSON.stringify(fifthCampSortieState)}`); } await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); for (let i = 0; i < 26; i += 1) { const isSixthBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'sixth-battle-jieqiao-relief'; }); if (isSixthBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-sixth-battle.png', fullPage: true }); const sixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const sixthEnemies = sixthBattleState.units.filter((unit) => unit.faction === 'enemy'); const sixthEnemyBehaviors = new Set(sixthEnemies.map((unit) => unit.ai)); if ( sixthBattleState.camera?.mapWidth !== 24 || sixthBattleState.camera?.mapHeight !== 20 || sixthBattleState.victoryConditionLabel !== '곡의 격파' || sixthEnemies.length < 12 || !sixthEnemyBehaviors.has('aggressive') || !sixthEnemyBehaviors.has('guard') || !sixthEnemyBehaviors.has('hold') ) { throw new Error(`Expected sixth battle map, objective, and mixed enemy AI: ${JSON.stringify(sixthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const campaignSaveAfterSixthBattle = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterSixthBattle || campaignSaveAfterSixthBattle.step !== 'sixth-camp' || campaignSaveAfterSixthBattle.latestBattleId !== 'sixth-battle-jieqiao-relief' || !campaignSaveAfterSixthBattle.battleHistory?.['sixth-battle-jieqiao-relief'] ) { throw new Error(`Expected campaign save to persist sixth battle victory: ${JSON.stringify(campaignSaveAfterSixthBattle)}`); } const sixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( sixthCampState?.campBattleId !== 'sixth-battle-jieqiao-relief' || sixthCampState.campTitle !== '서주 원군 준비 군영' || sixthCampState.availableDialogueIds?.length !== 3 || !sixthCampState.availableDialogueIds.every((id) => id.endsWith('jieqiao')) ) { throw new Error(`Expected sixth camp to expose Xu Province bridge dialogue set: ${JSON.stringify(sixthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const sixthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!sixthCampSortieState?.sortieVisible) { throw new Error(`Expected sixth camp sortie preparation overlay: ${JSON.stringify(sixthCampSortieState)}`); } if (!sixthCampSortieState.selectedSortieUnitIds?.includes('liu-bei') || sixthCampSortieState.selectedSortieUnitIds.length < 3) { throw new Error(`Expected sortie prep to default to the core brother roster: ${JSON.stringify(sixthCampSortieState)}`); } assertSortieTacticalRoster(sixthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei']); await clickSortieRosterUnit(page, 'guan-yu'); await page.waitForTimeout(220); const sixthCampAfterSortieToggle = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !sixthCampAfterSortieToggle?.sortieVisible || sixthCampAfterSortieToggle.selectedSortieUnitIds?.includes('guan-yu') || !sixthCampAfterSortieToggle.selectedSortieUnitIds?.includes('liu-bei') || !sixthCampAfterSortieToggle.selectedSortieUnitIds?.includes('zhang-fei') ) { throw new Error(`Expected sortie selection toggle to bench Guan Yu while keeping Liu Bei/Zhang Fei: ${JSON.stringify(sixthCampAfterSortieToggle)}`); } await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-seventh-bridge-story.png', fullPage: true }); for (let i = 0; i < 30; i += 1) { const isSeventhBattleActive = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'seventh-battle-xuzhou-rescue'; }); if (isSeventhBattleActive) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-seventh-battle.png', fullPage: true }); const seventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const seventhEnemies = seventhBattleState.units.filter((unit) => unit.faction === 'enemy'); const seventhAllies = seventhBattleState.units.filter((unit) => unit.faction === 'ally'); const seventhEnemyBehaviors = new Set(seventhEnemies.map((unit) => unit.ai)); if ( seventhBattleState.camera?.mapWidth !== 24 || seventhBattleState.camera?.mapHeight !== 20 || seventhBattleState.victoryConditionLabel !== '하후돈 격파' || seventhEnemies.length < 12 || !seventhEnemyBehaviors.has('aggressive') || !seventhEnemyBehaviors.has('guard') || !seventhEnemyBehaviors.has('hold') || seventhAllies.some((unit) => unit.id === 'guan-yu') || !seventhAllies.some((unit) => unit.id === 'liu-bei') || !seventhAllies.some((unit) => unit.id === 'zhang-fei') ) { throw new Error(`Expected seventh battle map, objective, mixed AI, and saved sortie selection: ${JSON.stringify(seventhBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const campaignSaveAfterSeventhBattle = await page.evaluate(() => { const raw = window.localStorage.getItem('heros-web:campaign-state'); return raw ? JSON.parse(raw) : undefined; }); if ( !campaignSaveAfterSeventhBattle || campaignSaveAfterSeventhBattle.step !== 'seventh-camp' || campaignSaveAfterSeventhBattle.latestBattleId !== 'seventh-battle-xuzhou-rescue' || !campaignSaveAfterSeventhBattle.battleHistory?.['seventh-battle-xuzhou-rescue'] || !campaignSaveAfterSeventhBattle.roster?.some((unit) => unit.id === 'guan-yu') ) { throw new Error(`Expected campaign save to persist seventh battle victory and preserve benched roster: ${JSON.stringify(campaignSaveAfterSeventhBattle)}`); } const seventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( seventhCampState?.campBattleId !== 'seventh-battle-xuzhou-rescue' || seventhCampState.campTitle !== '서주 인수 준비 군영' || seventhCampState.availableDialogueIds?.length !== 5 || !seventhCampState.availableDialogueIds.every((id) => id.endsWith('xuzhou')) || !seventhCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || !seventhCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !seventhCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected seventh camp to expose Xu Province dialogue set, preserved roster, and new recruits: ${JSON.stringify(seventhCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const seventhCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!seventhCampSortieState?.sortieVisible) { throw new Error(`Expected seventh camp story bridge overlay: ${JSON.stringify(seventhCampSortieState)}`); } await clickSortieRosterUnit(page, 'jian-yong'); await page.waitForTimeout(120); const seventhCampRecruitSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!seventhCampRecruitSortieState.selectedSortieUnitIds?.includes('jian-yong')) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-xuzhou-entrust-story.png', fullPage: true }); for (let i = 0; i < 10; i += 1) { const enteredEighthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'eighth-battle-xiaopei-supply-road'; }); if (enteredEighthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-eighth-battle.png', fullPage: true }); const eighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const eighthEnemies = eighthBattleState.units.filter((unit) => unit.faction === 'enemy'); const eighthAllies = eighthBattleState.units.filter((unit) => unit.faction === 'ally'); const eighthEnemyBehaviors = new Set(eighthEnemies.map((unit) => unit.ai)); if ( eighthBattleState.camera?.mapWidth !== 26 || eighthBattleState.camera?.mapHeight !== 22 || eighthBattleState.victoryConditionLabel !== '고순 격파' || eighthEnemies.length < 11 || !eighthEnemyBehaviors.has('aggressive') || !eighthEnemyBehaviors.has('guard') || !eighthEnemyBehaviors.has('hold') || eighthAllies.some((unit) => unit.id === 'guan-yu') || !eighthAllies.some((unit) => unit.id === 'liu-bei') || !eighthAllies.some((unit) => unit.id === 'zhang-fei') || !eighthAllies.some((unit) => unit.id === 'jian-yong') ) { throw new Error(`Expected eighth battle to use expanded map, mixed AI, and selected recruit sortie: ${JSON.stringify(eighthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const eighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( eighthCampState?.campBattleId !== 'eighth-battle-xiaopei-supply-road' || eighthCampState.campTitle !== '소패 방위 군영' || eighthCampState.availableDialogueIds?.length !== 3 || !eighthCampState.availableDialogueIds.every((id) => id.endsWith('xiaopei')) || !eighthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !eighthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected eighth camp to expose Xiaopei dialogue set and preserve recruits: ${JSON.stringify(eighthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const eighthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!eighthCampSortieState?.sortieVisible) { throw new Error(`Expected eighth camp sortie prep overlay: ${JSON.stringify(eighthCampSortieState)}`); } if (!eighthCampSortieState.selectedSortieUnitIds?.includes('mi-zhu')) { await clickSortieRosterUnit(page, 'mi-zhu'); await page.waitForTimeout(120); } const eighthCampMiZhuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!eighthCampMiZhuSortieState.selectedSortieUnitIds?.includes('mi-zhu')) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-lubu-refuge-story.png', fullPage: true }); for (let i = 0; i < 10; i += 1) { const enteredNinthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'ninth-battle-xuzhou-gate-night-raid'; }); if (enteredNinthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-ninth-battle.png', fullPage: true }); const ninthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const ninthEnemies = ninthBattleState.units.filter((unit) => unit.faction === 'enemy'); const ninthAllies = ninthBattleState.units.filter((unit) => unit.faction === 'ally'); const ninthEnemyBehaviors = new Set(ninthEnemies.map((unit) => unit.ai)); if ( ninthBattleState.camera?.mapWidth !== 26 || ninthBattleState.camera?.mapHeight !== 22 || ninthBattleState.victoryConditionLabel !== '조표 격파' || ninthEnemies.length < 10 || !ninthEnemyBehaviors.has('aggressive') || !ninthEnemyBehaviors.has('guard') || !ninthEnemyBehaviors.has('hold') || ninthAllies.some((unit) => unit.id === 'guan-yu') || !ninthAllies.some((unit) => unit.id === 'liu-bei') || !ninthAllies.some((unit) => unit.id === 'zhang-fei') || !ninthAllies.some((unit) => unit.id === 'jian-yong') || !ninthAllies.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected ninth battle to use gate map, mixed AI, and selected recruit sortie: ${JSON.stringify(ninthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const ninthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( ninthCampState?.campBattleId !== 'ninth-battle-xuzhou-gate-night-raid' || ninthCampState.campTitle !== '서주 성문 군영' || ninthCampState.availableDialogueIds?.length !== 3 || !ninthCampState.availableDialogueIds.every((id) => id.endsWith('xuzhou-gate')) || !ninthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !ninthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected ninth camp to expose Xuzhou gate dialogue set and preserve recruits: ${JSON.stringify(ninthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const ninthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!ninthCampSortieState?.sortieVisible) { throw new Error(`Expected ninth camp sortie prep overlay: ${JSON.stringify(ninthCampSortieState)}`); } if (!ninthCampSortieState.selectedSortieUnitIds?.includes('guan-yu')) { await clickSortieRosterUnit(page, 'guan-yu'); await page.waitForTimeout(120); } const ninthCampGuanYuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!ninthCampGuanYuSortieState.selectedSortieUnitIds?.includes('guan-yu')) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-xuzhou-loss-story.png', fullPage: true }); for (let i = 0; i < 14; i += 1) { const enteredTenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'tenth-battle-xuzhou-breakout'; }); if (enteredTenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-tenth-battle.png', fullPage: true }); const tenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const tenthEnemies = tenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const tenthAllies = tenthBattleState.units.filter((unit) => unit.faction === 'ally'); const tenthEnemyBehaviors = new Set(tenthEnemies.map((unit) => unit.ai)); if ( tenthBattleState.camera?.mapWidth !== 28 || tenthBattleState.camera?.mapHeight !== 22 || tenthBattleState.victoryConditionLabel !== '송헌 격파' || tenthEnemies.length < 11 || !tenthEnemyBehaviors.has('aggressive') || !tenthEnemyBehaviors.has('guard') || !tenthEnemyBehaviors.has('hold') || !tenthAllies.some((unit) => unit.id === 'liu-bei') || !tenthAllies.some((unit) => unit.id === 'guan-yu') || !tenthAllies.some((unit) => unit.id === 'zhang-fei') || !tenthAllies.some((unit) => unit.id === 'jian-yong') || !tenthAllies.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected tenth battle to use breakout map, mixed AI, and selected full Xu Province sortie: ${JSON.stringify(tenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const tenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( tenthCampState?.campBattleId !== 'tenth-battle-xuzhou-breakout' || tenthCampState.campTitle !== '서주 상실 후 군영' || tenthCampState.availableDialogueIds?.length !== 3 || !tenthCampState.availableDialogueIds.every((id) => id.endsWith('xuzhou-loss')) || !tenthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || !tenthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !tenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected tenth camp to expose Xuzhou loss dialogue set and preserve full roster: ${JSON.stringify(tenthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const tenthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!tenthCampSortieState?.sortieVisible) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-cao-cao-refuge-story.png', fullPage: true }); for (let i = 0; i < 16; i += 1) { const enteredEleventhBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'eleventh-battle-xudu-refuge-road'; }); if (enteredEleventhBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-eleventh-battle.png', fullPage: true }); const eleventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const eleventhEnemies = eleventhBattleState.units.filter((unit) => unit.faction === 'enemy'); const eleventhAllies = eleventhBattleState.units.filter((unit) => unit.faction === 'ally'); const eleventhEnemyBehaviors = new Set(eleventhEnemies.map((unit) => unit.ai)); if ( eleventhBattleState.camera?.mapWidth !== 28 || eleventhBattleState.camera?.mapHeight !== 22 || eleventhBattleState.victoryConditionLabel !== '기령 격파' || eleventhEnemies.length < 12 || !eleventhEnemyBehaviors.has('aggressive') || !eleventhEnemyBehaviors.has('guard') || !eleventhEnemyBehaviors.has('hold') || !eleventhAllies.some((unit) => unit.id === 'liu-bei') || !eleventhAllies.some((unit) => unit.id === 'guan-yu') || !eleventhAllies.some((unit) => unit.id === 'zhang-fei') || !eleventhAllies.some((unit) => unit.id === 'jian-yong') || !eleventhAllies.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected eleventh battle to use Xudu road map, mixed AI, and selected Cao Cao refuge sortie: ${JSON.stringify(eleventhBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const eleventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( eleventhCampState?.campBattleId !== 'eleventh-battle-xudu-refuge-road' || eleventhCampState.campTitle !== '허도 의탁 군영' || eleventhCampState.availableDialogueIds?.length !== 3 || !eleventhCampState.availableDialogueIds.every((id) => id.endsWith('cao-cao-refuge')) || !eleventhCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || !eleventhCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !eleventhCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected eleventh camp to expose Cao Cao refuge dialogue set and preserve full roster: ${JSON.stringify(eleventhCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const eleventhCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!eleventhCampSortieState?.sortieVisible || !eleventhCampSortieState.sortiePlan?.objectiveLine?.includes('하비 외곽전')) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-xiapi-siege-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { const enteredTwelfthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twelfth-battle-xiapi-outer-siege'; }); if (enteredTwelfthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twelfth-battle.png', fullPage: true }); const twelfthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twelfthEnemies = twelfthBattleState.units.filter((unit) => unit.faction === 'enemy'); const twelfthAllies = twelfthBattleState.units.filter((unit) => unit.faction === 'ally'); const twelfthEnemyBehaviors = new Set(twelfthEnemies.map((unit) => unit.ai)); if ( twelfthBattleState.camera?.mapWidth !== 30 || twelfthBattleState.camera?.mapHeight !== 24 || twelfthBattleState.victoryConditionLabel !== '장료 격파' || twelfthEnemies.length < 13 || !twelfthEnemyBehaviors.has('aggressive') || !twelfthEnemyBehaviors.has('guard') || !twelfthEnemyBehaviors.has('hold') || !twelfthEnemies.some((unit) => unit.id === 'xiapi-strategist-chen-gong') || !twelfthAllies.some((unit) => unit.id === 'liu-bei') || !twelfthAllies.some((unit) => unit.id === 'guan-yu') || !twelfthAllies.some((unit) => unit.id === 'zhang-fei') || !twelfthAllies.some((unit) => unit.id === 'jian-yong') || !twelfthAllies.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected twelfth battle to use Xiapi siege map, mixed AI, and selected full sortie: ${JSON.stringify(twelfthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twelfthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twelfthCampState?.campBattleId !== 'twelfth-battle-xiapi-outer-siege' || twelfthCampState.campTitle !== '하비 포위 군영' || twelfthCampState.availableDialogueIds?.length !== 3 || !twelfthCampState.availableDialogueIds.every((id) => id.endsWith('xiapi-outer')) || !twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || !twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected twelfth camp to expose Xiapi siege dialogue set and preserve full roster: ${JSON.stringify(twelfthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const twelfthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !twelfthCampSortieState?.sortieVisible || !twelfthCampSortieState.sortiePlan?.objectiveLine?.includes('하비 결전') || twelfthCampSortieState.sortiePlan?.selectedRecruitedCount < 1 || !twelfthCampSortieState.sortiePlan?.recruitedLine?.includes('합류 무장') ) { throw new Error(`Expected twelfth camp sortie prep to target Xiapi final battle with recruit selection summary: ${JSON.stringify(twelfthCampSortieState)}`); } assertSortieTacticalRoster(twelfthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-xiapi-final-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { const enteredThirteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirteenth-battle-xiapi-final'; }); if (enteredThirteenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirteenth-battle.png', fullPage: true }); const thirteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirteenthEnemies = thirteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirteenthAllies = thirteenthBattleState.units.filter((unit) => unit.faction === 'ally'); const thirteenthEnemyBehaviors = new Set(thirteenthEnemies.map((unit) => unit.ai)); if ( thirteenthBattleState.camera?.mapWidth !== 32 || thirteenthBattleState.camera?.mapHeight !== 24 || thirteenthBattleState.victoryConditionLabel !== '여포 격파' || thirteenthEnemies.length < 15 || !thirteenthEnemyBehaviors.has('aggressive') || !thirteenthEnemyBehaviors.has('guard') || !thirteenthEnemyBehaviors.has('hold') || !thirteenthEnemies.some((unit) => unit.id === 'xiapi-final-leader-lu-bu') || !thirteenthEnemies.some((unit) => unit.id === 'xiapi-final-strategist-chen-gong') || !thirteenthAllies.some((unit) => unit.id === 'liu-bei') || !thirteenthAllies.some((unit) => unit.id === 'guan-yu') || !thirteenthAllies.some((unit) => unit.id === 'zhang-fei') || !thirteenthAllies.some((unit) => unit.id === 'jian-yong') || !thirteenthAllies.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected thirteenth battle to use Xiapi final map, Lu Bu objective, mixed AI, and selected recruit sortie: ${JSON.stringify(thirteenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirteenthCampState?.campBattleId !== 'thirteenth-battle-xiapi-final' || thirteenthCampState.campTitle !== '하비 결전 후 군영' || thirteenthCampState.availableDialogueIds?.length !== 3 || !thirteenthCampState.availableDialogueIds.every((id) => id.endsWith('lu-bu-fall')) || !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') || !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') ) { throw new Error(`Expected thirteenth camp to expose Lu Bu fall dialogue set, recruit Sun Qian, and preserve full roster: ${JSON.stringify(thirteenthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const thirteenthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !thirteenthCampSortieState?.sortieVisible || !thirteenthCampSortieState.sortiePlan?.objectiveLine?.includes('서주 재기') || !thirteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) ) { throw new Error(`Expected thirteenth camp sortie prep to target Cao break battle with Sun Qian as a recruit option: ${JSON.stringify(thirteenthCampSortieState)}`); } assertSortieTacticalRoster(thirteenthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian']); await clickSortieRosterUnit(page, 'sun-qian'); const thirteenthCampSunQianSelectedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!thirteenthCampSunQianSelectedState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.selected)) { throw new Error(`Expected Sun Qian to be selectable for the fourteenth battle sortie: ${JSON.stringify(thirteenthCampSunQianSelectedState)}`); } await page.screenshot({ path: 'dist/verification-cao-break-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-cao-break-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { const enteredFourteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'fourteenth-battle-cao-break'; }); if (enteredFourteenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-fourteenth-battle.png', fullPage: true }); const fourteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const fourteenthEnemies = fourteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const fourteenthAllies = fourteenthBattleState.units.filter((unit) => unit.faction === 'ally'); const fourteenthEnemyBehaviors = new Set(fourteenthEnemies.map((unit) => unit.ai)); if ( fourteenthBattleState.camera?.mapWidth !== 32 || fourteenthBattleState.camera?.mapHeight !== 24 || fourteenthBattleState.victoryConditionLabel !== '차주 격파' || fourteenthEnemies.length < 15 || !fourteenthEnemyBehaviors.has('aggressive') || !fourteenthEnemyBehaviors.has('guard') || !fourteenthEnemyBehaviors.has('hold') || !fourteenthEnemies.some((unit) => unit.id === 'cao-break-leader-che-zhou') || !fourteenthAllies.some((unit) => unit.id === 'sun-qian') ) { throw new Error(`Expected fourteenth battle to use Cao break map, Che Zhou objective, mixed AI, and selected Sun Qian sortie: ${JSON.stringify(fourteenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const fourteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( fourteenthCampState?.campBattleId !== 'fourteenth-battle-cao-break' || fourteenthCampState.campTitle !== '서주 재기 후 군영' || fourteenthCampState.availableDialogueIds?.length !== 3 || !fourteenthCampState.availableDialogueIds.every((id) => id.endsWith('cao-break')) || !fourteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') ) { throw new Error(`Expected fourteenth camp to expose Cao break dialogue set and preserve Sun Qian in roster: ${JSON.stringify(fourteenthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const fourteenthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fourteenthCampSortieState?.sortieVisible || !fourteenthCampSortieState.sortiePlan?.objectiveLine?.includes('원소 의탁로') || !fourteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited) ) { throw new Error(`Expected fourteenth camp sortie prep to target Yuan Shao refuge road with expanded roster: ${JSON.stringify(fourteenthCampSortieState)}`); } 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-yuan-refuge-story.png', fullPage: true }); for (let i = 0; i < 18; i += 1) { const enteredFifteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'fifteenth-battle-yuan-refuge-road'; }); if (enteredFifteenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-fifteenth-battle.png', fullPage: true }); const fifteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const fifteenthEnemies = fifteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const fifteenthAllies = fifteenthBattleState.units.filter((unit) => unit.faction === 'ally'); const fifteenthEnemyBehaviors = new Set(fifteenthEnemies.map((unit) => unit.ai)); if ( fifteenthBattleState.camera?.mapWidth !== 34 || fifteenthBattleState.camera?.mapHeight !== 24 || fifteenthBattleState.victoryConditionLabel !== '채양 격파' || fifteenthEnemies.length < 16 || !fifteenthEnemyBehaviors.has('aggressive') || !fifteenthEnemyBehaviors.has('guard') || !fifteenthEnemyBehaviors.has('hold') || !fifteenthEnemies.some((unit) => unit.id === 'yuan-refuge-leader-cai-yang') || !fifteenthAllies.some((unit) => unit.id === 'sun-qian') ) { throw new Error(`Expected fifteenth battle to use Yuan refuge map, Cai Yang objective, mixed AI, and expanded sortie: ${JSON.stringify(fifteenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const fifteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( fifteenthCampState?.campBattleId !== 'fifteenth-battle-yuan-refuge-road' || fifteenthCampState.campTitle !== '원소 의탁 후 군영' || fifteenthCampState.availableDialogueIds?.length !== 3 || !fifteenthCampState.availableDialogueIds.every((id) => id.endsWith('yuan-refuge')) || !fifteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') || !fifteenthCampState.campaign?.roster?.some((unit) => unit.id === 'zhao-yun') ) { throw new Error(`Expected fifteenth camp to expose Yuan refuge dialogue set, preserve expanded roster, and recruit Zhao Yun: ${JSON.stringify(fifteenthCampState)}`); } await page.mouse.click(1120, 38); await page.waitForTimeout(160); const fifteenthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fifteenthCampSortieState?.sortieVisible || !fifteenthCampSortieState.sortiePlan?.objectiveLine?.includes('유표 의탁로') || !fifteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || fifteenthCampSortieState.sortieRoster?.length < 7 || fifteenthCampSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected fifteenth camp sortie prep to target Liu Biao refuge road with Zhao Yun as a selectable recruit: ${JSON.stringify(fifteenthCampSortieState)}`); } assertSortieTacticalRoster(fifteenthCampSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun' ]); if (fifteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected)) { await clickSortieRosterUnit(page, 'zhao-yun'); } const fifteenthCampBeforeSwapState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (fifteenthCampBeforeSwapState.sortieRoster?.some((unit) => unit.id === 'jian-yong' && unit.selected)) { await clickSortieRosterUnit(page, 'jian-yong'); } await clickSortieRosterUnit(page, 'zhao-yun'); const fifteenthCampZhaoYunSelectedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fifteenthCampZhaoYunSelectedState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) || fifteenthCampZhaoYunSelectedState.sortieRoster?.some((unit) => unit.id === 'jian-yong' && unit.selected) || fifteenthCampZhaoYunSelectedState.sortiePlan?.selectedCount !== 6 ) { throw new Error(`Expected Zhao Yun to replace Jian Yong in sixteenth battle sortie: ${JSON.stringify(fifteenthCampZhaoYunSelectedState)}`); } await page.screenshot({ path: 'dist/verification-liu-biao-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-liu-biao-story.png', fullPage: true }); for (let i = 0; i < 22; i += 1) { const enteredSixteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'sixteenth-battle-liu-biao-refuge'; }); if (enteredSixteenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-sixteenth-battle.png', fullPage: true }); const sixteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const sixteenthEnemies = sixteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const sixteenthAllies = sixteenthBattleState.units.filter((unit) => unit.faction === 'ally'); const sixteenthEnemyBehaviors = new Set(sixteenthEnemies.map((unit) => unit.ai)); if ( sixteenthBattleState.camera?.mapWidth !== 34 || sixteenthBattleState.camera?.mapHeight !== 26 || sixteenthBattleState.victoryConditionLabel !== '조인 격파' || sixteenthEnemies.length < 18 || !sixteenthEnemyBehaviors.has('aggressive') || !sixteenthEnemyBehaviors.has('guard') || !sixteenthEnemyBehaviors.has('hold') || !sixteenthEnemies.some((unit) => unit.id === 'liu-biao-road-leader-cao-ren') || !sixteenthAllies.some((unit) => unit.id === 'zhao-yun') || sixteenthAllies.some((unit) => unit.id === 'jian-yong') ) { throw new Error(`Expected sixteenth battle to use Liu Biao refuge map, Cao Ren objective, mixed AI, and selected Zhao Yun sortie: ${JSON.stringify(sixteenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const sixteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( sixteenthCampState?.campBattleId !== 'sixteenth-battle-liu-biao-refuge' || sixteenthCampState.campTitle !== '형주 의탁 후 군영' || sixteenthCampState.availableDialogueIds?.length !== 3 || !sixteenthCampState.availableDialogueIds.every((id) => id.endsWith('liu-biao-refuge')) || sixteenthCampState.availableVisitIds?.length !== 3 || !sixteenthCampState.campaign?.roster?.some((unit) => unit.id === 'zhao-yun') ) { 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.waitForTimeout(160); const sixteenthVisitTabState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( sixteenthVisitTabState?.activeTab !== 'visit' || !sixteenthVisitTabState.availableVisitIds?.includes('jingzhou-scholar-rumors') || sixteenthVisitTabState.completedAvailableVisits?.length !== 0 ) { throw new Error(`Expected Liu Biao camp visit tab to expose local visit choices: ${JSON.stringify(sixteenthVisitTabState)}`); } await page.mouse.click(974, 436); await page.waitForTimeout(260); const sixteenthVisitRewardState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !sixteenthVisitRewardState.completedAvailableVisits?.includes('jingzhou-scholar-rumors') || !sixteenthVisitRewardState.campaign?.completedCampVisits?.includes('jingzhou-scholar-rumors') || (sixteenthVisitRewardState.campaign?.inventory?.['와룡 소문'] ?? 0) < 1 ) { throw new Error(`Expected completed visit to persist and award Wolong clue inventory: ${JSON.stringify(sixteenthVisitRewardState)}`); } await page.screenshot({ path: 'dist/verification-liu-biao-visits.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(160); const sixteenthCampWolongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !sixteenthCampWolongSortieState?.sortieVisible || !sixteenthCampWolongSortieState.sortiePlan?.objectiveLine?.includes('융중 방문로') || !sixteenthCampWolongSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !sixteenthCampWolongSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || sixteenthCampWolongSortieState.sortieRoster?.length < 7 || sixteenthCampWolongSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected sixteenth camp sortie prep to target Wolong visit road with expanded selectable roster: ${JSON.stringify(sixteenthCampWolongSortieState)}`); } assertSortieTacticalRoster(sixteenthCampWolongSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun' ]); await page.screenshot({ path: 'dist/verification-wolong-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-wolong-story.png', fullPage: true }); for (let i = 0; i < 24; i += 1) { const enteredSeventeenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'seventeenth-battle-wolong-visit-road'; }); if (enteredSeventeenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-seventeenth-battle.png', fullPage: true }); const seventeenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const seventeenthEnemies = seventeenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const seventeenthAllies = seventeenthBattleState.units.filter((unit) => unit.faction === 'ally'); const seventeenthEnemyBehaviors = new Set(seventeenthEnemies.map((unit) => unit.ai)); if ( seventeenthBattleState.camera?.mapWidth !== 34 || seventeenthBattleState.camera?.mapHeight !== 26 || seventeenthBattleState.victoryConditionLabel !== '채순 격파' || seventeenthEnemies.length < 15 || !seventeenthEnemyBehaviors.has('aggressive') || !seventeenthEnemyBehaviors.has('guard') || !seventeenthEnemyBehaviors.has('hold') || !seventeenthEnemies.some((unit) => unit.id === 'wolong-road-leader-cai-xun') || !seventeenthAllies.some((unit) => unit.id === 'zhao-yun') || seventeenthAllies.some((unit) => unit.id === 'zhuge-liang') ) { throw new Error(`Expected seventeenth battle to use Wolong visit map, Cai Xun objective, mixed AI, and pre-Zhuge sortie: ${JSON.stringify(seventeenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const seventeenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( seventeenthCampState?.campBattleId !== 'seventeenth-battle-wolong-visit-road' || seventeenthCampState.campTitle !== '와룡 출려 후 군영' || seventeenthCampState.availableDialogueIds?.length !== 3 || !seventeenthCampState.availableDialogueIds.every((id) => id.endsWith('wolong-visit')) || seventeenthCampState.availableVisitIds?.length !== 2 || !seventeenthCampState.campaign?.roster?.some((unit) => unit.id === 'zhuge-liang') || !seventeenthCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__zhuge-liang') || !seventeenthCampState.report?.bonds?.some((bond) => bond.id === 'guan-yu__zhuge-liang') ) { throw new Error(`Expected seventeenth camp to recruit Zhuge Liang and expose Wolong dialogue/visit sets: ${JSON.stringify(seventeenthCampState)}`); } await page.screenshot({ path: 'dist/verification-zhuge-recruit-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const progressTabState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( progressTabState?.activeTab !== 'progress' || progressTabState.campaignProgress?.completedKnown !== 17 || progressTabState.campaignProgress?.totalKnown !== 38 || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' ) { throw new Error(`Expected progress tab to summarize Wolong completion and point toward Bowang ambush: ${JSON.stringify(progressTabState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-campaign-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(160); const seventeenthCampBowangSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !seventeenthCampBowangSortieState?.sortieVisible || !seventeenthCampBowangSortieState.sortiePlan?.objectiveLine?.includes('박망파 매복전') || !seventeenthCampBowangSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || seventeenthCampBowangSortieState.sortieRoster?.length < 8 || seventeenthCampBowangSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected seventeenth camp sortie prep to target Bowang ambush with Zhuge Liang as a selectable recruit: ${JSON.stringify(seventeenthCampBowangSortieState)}`); } assertSortieTacticalRoster(seventeenthCampBowangSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang' ]); if (!seventeenthCampBowangSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected)) { const selectedReserveCandidate = ['jian-yong', 'mi-zhu'].find((unitId) => seventeenthCampBowangSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ); if (selectedReserveCandidate) { await clickSortieRosterUnit(page, selectedReserveCandidate); } await clickSortieRosterUnit(page, 'zhuge-liang'); } const bowangSortieWithZhugeState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !bowangSortieWithZhugeState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected) || bowangSortieWithZhugeState.sortiePlan?.selectedCount !== 6 || bowangSortieWithZhugeState.sortiePlan?.recommendedSelectedCount < 4 ) { throw new Error(`Expected Zhuge Liang to be selectable and deployed for Bowang ambush: ${JSON.stringify(bowangSortieWithZhugeState)}`); } await page.screenshot({ path: 'dist/verification-bowang-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-bowang-story.png', fullPage: true }); for (let i = 0; i < 26; i += 1) { const enteredEighteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'eighteenth-battle-bowang-ambush'; }); if (enteredEighteenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-eighteenth-battle.png', fullPage: true }); const eighteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const eighteenthEnemies = eighteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const eighteenthAllies = eighteenthBattleState.units.filter((unit) => unit.faction === 'ally'); const eighteenthEnemyBehaviors = new Set(eighteenthEnemies.map((unit) => unit.ai)); const treasureLabels = new Set( eighteenthBattleState.treasureEffectPreviews?.flatMap((preview) => preview.equipmentEffectLabels ?? []) ?? [] ); if ( eighteenthBattleState.camera?.mapWidth !== 36 || eighteenthBattleState.camera?.mapHeight !== 26 || eighteenthBattleState.victoryConditionLabel !== '하후돈 퇴각' || eighteenthEnemies.length < 16 || !eighteenthEnemyBehaviors.has('aggressive') || !eighteenthEnemyBehaviors.has('guard') || !eighteenthEnemyBehaviors.has('hold') || !eighteenthEnemies.some((unit) => unit.id === 'bowang-leader-xiahou-dun') || !eighteenthAllies.some((unit) => unit.id === 'zhuge-liang') || !treasureLabels.has('자웅일대검 공명') || (!treasureLabels.has('청룡언월도 무력 우위') && !treasureLabels.has('청룡언월도 대도')) || !treasureLabels.has('장팔사모 치명') || !treasureLabels.has('백우선 책략') ) { throw new Error(`Expected eighteenth battle to use Bowang map, Xiahou Dun objective, mixed AI, deployed Zhuge Liang, and treasure equipment effects: ${JSON.stringify(eighteenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const eighteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( eighteenthCampState?.campBattleId !== 'eighteenth-battle-bowang-ambush' || eighteenthCampState.campTitle !== '박망파 승전 후 군영' || eighteenthCampState.availableDialogueIds?.length !== 3 || !eighteenthCampState.availableDialogueIds.every((id) => id.endsWith('bowang-ambush')) || eighteenthCampState.availableVisitIds?.length !== 2 || !eighteenthCampState.report?.bonds?.some((bond) => bond.id === 'zhang-fei__zhuge-liang') || !eighteenthCampState.report?.bonds?.some((bond) => bond.id === 'sun-qian__zhuge-liang') || eighteenthCampState.rosterCollection?.total < 8 || eighteenthCampState.rosterCollection?.reserveTrainingCount < 2 || 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)) ) { 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.waitForTimeout(160); await page.mouse.click(180, 315); await page.waitForTimeout(160); const bowangRosterState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( bowangRosterState?.activeTab !== 'status' || bowangRosterState.selectedUnitId !== 'jian-yong' || bowangRosterState.rosterCollection?.reserveCount < 2 || !bowangRosterState.reserveTrainingAwards?.some((entry) => entry.unitId === 'jian-yong' && entry.expGained === 6) ) { throw new Error(`Expected status tab to expose officer roster reserve growth details: ${JSON.stringify(bowangRosterState)}`); } await page.screenshot({ path: 'dist/verification-bowang-roster.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postBowangProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postBowangProgressState?.activeTab !== 'progress' || postBowangProgressState.campaignProgress?.completedKnown !== 18 || postBowangProgressState.campaignProgress?.totalKnown !== 38 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' ) { throw new Error(`Expected post-Bowang progress tab to point toward Changban refuge route: ${JSON.stringify(postBowangProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-bowang-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const changbanSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !changbanSortieState?.sortieVisible || !changbanSortieState.sortiePlan?.objectiveLine?.includes('장판파 피난로') || !changbanSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !changbanSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || !changbanSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || changbanSortieState.sortieRoster?.length < 8 || changbanSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected eighteenth camp sortie prep to target Changban with Zhao Yun, Zhang Fei, and Zhuge Liang recommendations: ${JSON.stringify(changbanSortieState)}`); } assertSortieTacticalRoster(changbanSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang' ]); const changbanPriorityUnits = ['zhao-yun', 'zhang-fei', 'zhuge-liang']; for (const unitId of changbanPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !changbanPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const changbanSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !changbanPriorityUnits.every((unitId) => changbanSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || changbanSortieReadyState.sortiePlan?.selectedCount !== 6 || changbanSortieReadyState.sortiePlan?.recommendedSelectedCount < 5 ) { throw new Error(`Expected Changban sortie to deploy priority officers and preserve six-officer choice pressure: ${JSON.stringify(changbanSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-changban-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-changban-story.png', fullPage: true }); for (let i = 0; i < 30; i += 1) { const enteredNineteenthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'nineteenth-battle-changban-refuge'; }); if (enteredNineteenthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-nineteenth-battle.png', fullPage: true }); const nineteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const nineteenthEnemies = nineteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); const nineteenthAllies = nineteenthBattleState.units.filter((unit) => unit.faction === 'ally'); const nineteenthEnemyBehaviors = new Set(nineteenthEnemies.map((unit) => unit.ai)); if ( nineteenthBattleState.camera?.mapWidth !== 38 || nineteenthBattleState.camera?.mapHeight !== 28 || nineteenthBattleState.victoryConditionLabel !== '조순 퇴각' || nineteenthEnemies.length < 17 || !nineteenthEnemyBehaviors.has('aggressive') || !nineteenthEnemyBehaviors.has('guard') || !nineteenthEnemyBehaviors.has('hold') || !nineteenthEnemies.some((unit) => unit.id === 'changban-leader-cao-chun') || !nineteenthAllies.some((unit) => unit.id === 'zhao-yun') || !nineteenthAllies.some((unit) => unit.id === 'zhuge-liang') ) { throw new Error(`Expected nineteenth battle to use Changban map, Cao Chun objective, mixed AI, Zhao Yun, and Zhuge Liang: ${JSON.stringify(nineteenthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const nineteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( nineteenthCampState?.campBattleId !== 'nineteenth-battle-changban-refuge' || nineteenthCampState.campTitle !== '장판파 돌파 후 군영' || nineteenthCampState.availableDialogueIds?.length !== 3 || !nineteenthCampState.availableDialogueIds.every((id) => id.endsWith('changban-refuge')) || nineteenthCampState.availableVisitIds?.length !== 2 || !nineteenthCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__zhao-yun') || !nineteenthCampState.report?.bonds?.some((bond) => bond.id === 'zhang-fei__zhuge-liang') || nineteenthCampState.rosterCollection?.total < 8 || nineteenthCampState.rosterCollection?.reserveTrainingCount < 2 || nineteenthCampState.rosterCollection?.reserveTrainingExp < 12 ) { throw new Error(`Expected nineteenth camp to expose Changban dialogue/visit sets, Zhao Yun bonds, and reserve growth: ${JSON.stringify(nineteenthCampState)}`); } await page.screenshot({ path: 'dist/verification-changban-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postChangbanProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postChangbanProgressState?.activeTab !== 'progress' || postChangbanProgressState.campaignProgress?.completedKnown !== 19 || postChangbanProgressState.campaignProgress?.totalKnown !== 38 || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' ) { throw new Error(`Expected post-Changban progress tab to point toward Jiangdong envoy route: ${JSON.stringify(postChangbanProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-changban-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const jiangdongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jiangdongSortieState?.sortieVisible || !jiangdongSortieState.sortiePlan?.objectiveLine?.includes('강동 사절로') || !jiangdongSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !jiangdongSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || !jiangdongSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || jiangdongSortieState.sortieRoster?.length < 8 || jiangdongSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected nineteenth camp sortie prep to target Jiangdong envoy with Zhuge Liang, Sun Qian, and Zhao Yun recommendations: ${JSON.stringify(jiangdongSortieState)}`); } assertSortieTacticalRoster(jiangdongSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang' ]); const jiangdongPriorityUnits = ['zhuge-liang', 'sun-qian', 'zhao-yun']; for (const unitId of jiangdongPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !jiangdongPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const jiangdongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jiangdongPriorityUnits.every((unitId) => jiangdongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || jiangdongSortieReadyState.sortiePlan?.selectedCount !== 6 || jiangdongSortieReadyState.sortiePlan?.recommendedSelectedCount < 5 ) { throw new Error(`Expected Jiangdong envoy sortie to deploy priority officers while preserving six-officer pressure: ${JSON.stringify(jiangdongSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-jiangdong-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-jiangdong-story.png', fullPage: true }); for (let i = 0; i < 32; i += 1) { const enteredTwentiethBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twentieth-battle-jiangdong-envoy'; }); if (enteredTwentiethBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twentieth-battle.png', fullPage: true }); const twentiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentiethEnemies = twentiethBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentiethAllies = twentiethBattleState.units.filter((unit) => unit.faction === 'ally'); const twentiethEnemyBehaviors = new Set(twentiethEnemies.map((unit) => unit.ai)); if ( twentiethBattleState.camera?.mapWidth !== 40 || twentiethBattleState.camera?.mapHeight !== 28 || twentiethBattleState.victoryConditionLabel !== '문빙 퇴각' || twentiethEnemies.length < 17 || !twentiethEnemyBehaviors.has('aggressive') || !twentiethEnemyBehaviors.has('guard') || !twentiethEnemyBehaviors.has('hold') || !twentiethEnemies.some((unit) => unit.id === 'envoy-road-leader-wen-pin') || !twentiethAllies.some((unit) => unit.id === 'sun-qian') || !twentiethAllies.some((unit) => unit.id === 'zhuge-liang') ) { throw new Error(`Expected twentieth battle to use Jiangdong envoy map, Wen Pin objective, mixed AI, Sun Qian, and Zhuge Liang: ${JSON.stringify(twentiethBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentiethCampState?.campBattleId !== 'twentieth-battle-jiangdong-envoy' || twentiethCampState.campTitle !== '강동 사절 준비 군영' || twentiethCampState.availableDialogueIds?.length !== 3 || !twentiethCampState.availableDialogueIds.every((id) => id.endsWith('jiangdong-envoy')) || twentiethCampState.availableVisitIds?.length !== 2 || !twentiethCampState.report?.bonds?.some((bond) => bond.id === 'sun-qian__zhuge-liang') || !twentiethCampState.report?.bonds?.some((bond) => bond.id === 'zhao-yun__zhuge-liang') || twentiethCampState.rosterCollection?.total < 8 || twentiethCampState.rosterCollection?.reserveTrainingCount < 2 || twentiethCampState.rosterCollection?.reserveTrainingExp < 12 ) { throw new Error(`Expected twentieth camp to expose Jiangdong dialogue/visit sets, envoy bonds, and reserve growth: ${JSON.stringify(twentiethCampState)}`); } await page.screenshot({ path: 'dist/verification-jiangdong-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postJiangdongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postJiangdongProgressState?.activeTab !== 'progress' || postJiangdongProgressState.campaignProgress?.completedKnown !== 20 || postJiangdongProgressState.campaignProgress?.totalKnown !== 38 || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' ) { throw new Error(`Expected post-Jiangdong progress tab to keep Red Cliffs active while first Red Cliffs battle is pending: ${JSON.stringify(postJiangdongProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-jiangdong-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const redCliffsSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !redCliffsSortieState?.sortieVisible || !redCliffsSortieState.sortiePlan?.objectiveLine?.includes('적벽 전초전') || !redCliffsSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !redCliffsSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !redCliffsSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || !redCliffsSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || redCliffsSortieState.sortieRoster?.length < 8 || redCliffsSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twentieth camp sortie prep to target Red Cliffs vanguard with Zhuge Liang, Zhao Yun, Guan Yu, and Sun Qian recommendations: ${JSON.stringify(redCliffsSortieState)}`); } assertSortieTacticalRoster(redCliffsSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang' ]); const redCliffsPriorityUnits = ['zhuge-liang', 'zhao-yun', 'guan-yu', 'sun-qian']; for (const unitId of redCliffsPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !redCliffsPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const redCliffsSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !redCliffsPriorityUnits.every((unitId) => redCliffsSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || redCliffsSortieReadyState.sortiePlan?.selectedCount !== 6 || redCliffsSortieReadyState.sortiePlan?.recommendedSelectedCount < 5 ) { throw new Error(`Expected Red Cliffs sortie to deploy priority officers while preserving six-officer choice pressure: ${JSON.stringify(redCliffsSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-redcliffs-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-redcliffs-story.png', fullPage: true }); for (let i = 0; i < 34; i += 1) { const enteredTwentyFirstBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-first-battle-red-cliffs-vanguard'; }); if (enteredTwentyFirstBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-first-battle.png', fullPage: true }); const twentyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentyFirstEnemies = twentyFirstBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentyFirstAllies = twentyFirstBattleState.units.filter((unit) => unit.faction === 'ally'); const twentyFirstEnemyBehaviors = new Set(twentyFirstEnemies.map((unit) => unit.ai)); if ( twentyFirstBattleState.camera?.mapWidth !== 42 || twentyFirstBattleState.camera?.mapHeight !== 30 || twentyFirstBattleState.victoryConditionLabel !== '채모 퇴각' || twentyFirstEnemies.length < 17 || !twentyFirstEnemyBehaviors.has('aggressive') || !twentyFirstEnemyBehaviors.has('guard') || !twentyFirstEnemyBehaviors.has('hold') || !twentyFirstEnemies.some((unit) => unit.id === 'redcliff-vanguard-leader-cai-mao') || !twentyFirstAllies.some((unit) => unit.id === 'zhuge-liang') || !twentyFirstAllies.some((unit) => unit.id === 'zhao-yun') || !twentyFirstAllies.some((unit) => unit.id === 'sun-qian') ) { throw new Error(`Expected twenty-first battle to use Red Cliffs vanguard map, Cai Mao objective, mixed AI, and selected allied officers: ${JSON.stringify(twentyFirstBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentyFirstCampState?.campBattleId !== 'twenty-first-battle-red-cliffs-vanguard' || twentyFirstCampState.campTitle !== '적벽 전초전 후 군영' || twentyFirstCampState.availableDialogueIds?.length !== 3 || !twentyFirstCampState.availableDialogueIds.every((id) => id.endsWith('red-cliffs-vanguard')) || twentyFirstCampState.availableVisitIds?.length !== 2 || !twentyFirstCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__zhuge-liang') || !twentyFirstCampState.report?.bonds?.some((bond) => bond.id === 'sun-qian__zhuge-liang') || twentyFirstCampState.rosterCollection?.total < 8 || twentyFirstCampState.rosterCollection?.reserveTrainingCount < 2 || twentyFirstCampState.rosterCollection?.reserveTrainingExp < 12 ) { throw new Error(`Expected twenty-first camp to expose Red Cliffs dialogue/visit sets, alliance bonds, and reserve growth: ${JSON.stringify(twentyFirstCampState)}`); } await page.screenshot({ path: 'dist/verification-redcliffs-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postRedCliffsProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || postRedCliffsProgressState.campaignProgress?.totalKnown !== 38 || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' ) { throw new Error(`Expected post-Red Cliffs vanguard progress tab to complete known battles and keep fire attack preparation pending: ${JSON.stringify(postRedCliffsProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-redcliffs-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const fireAttackSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fireAttackSortieState?.sortieVisible || !fireAttackSortieState.sortiePlan?.objectiveLine?.includes('적벽 화공전') || !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !fireAttackSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && unit.recommended) || fireAttackSortieState.sortieRoster?.length < 8 || fireAttackSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-first camp sortie prep to target Red Cliffs fire attack with Zhuge Liang, Guan Yu, Zhao Yun, and Mi Zhu recommendations: ${JSON.stringify(fireAttackSortieState)}`); } assertSortieTacticalRoster(fireAttackSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang' ]); const fireAttackPriorityUnits = ['zhuge-liang', 'guan-yu', 'zhao-yun', 'mi-zhu']; for (const unitId of fireAttackPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !fireAttackPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const fireAttackSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fireAttackPriorityUnits.every((unitId) => fireAttackSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || fireAttackSortieReadyState.sortiePlan?.selectedCount !== 6 || fireAttackSortieReadyState.sortiePlan?.recommendedSelectedCount < 5 ) { throw new Error(`Expected Red Cliffs fire attack sortie to deploy priority officers while preserving six-officer choice pressure: ${JSON.stringify(fireAttackSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-redcliffs-fire-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-redcliffs-fire-story.png', fullPage: true }); for (let i = 0; i < 36; i += 1) { const enteredTwentySecondBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-second-battle-red-cliffs-fire'; }); if (enteredTwentySecondBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-second-battle.png', fullPage: true }); const twentySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentySecondEnemies = twentySecondBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentySecondAllies = twentySecondBattleState.units.filter((unit) => unit.faction === 'ally'); const twentySecondEnemyBehaviors = new Set(twentySecondEnemies.map((unit) => unit.ai)); if ( twentySecondBattleState.camera?.mapWidth !== 44 || twentySecondBattleState.camera?.mapHeight !== 30 || twentySecondBattleState.victoryConditionLabel !== '조조 본선 퇴각' || twentySecondEnemies.length < 18 || !twentySecondEnemyBehaviors.has('aggressive') || !twentySecondEnemyBehaviors.has('guard') || !twentySecondEnemyBehaviors.has('hold') || !twentySecondEnemies.some((unit) => unit.id === 'redcliff-fire-leader-cao-cao') || !twentySecondAllies.some((unit) => unit.id === 'zhuge-liang') || !twentySecondAllies.some((unit) => unit.id === 'zhao-yun') || !twentySecondAllies.some((unit) => unit.id === 'mi-zhu') ) { throw new Error(`Expected twenty-second battle to use Red Cliffs fire map, Cao Cao objective, mixed AI, and selected allied officers: ${JSON.stringify(twentySecondBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentySecondCampState?.campBattleId !== 'twenty-second-battle-red-cliffs-fire' || twentySecondCampState.campTitle !== '적벽 화공전 후 군영' || twentySecondCampState.availableDialogueIds?.length !== 3 || !twentySecondCampState.availableDialogueIds.every((id) => id.endsWith('red-cliffs-fire')) || twentySecondCampState.availableVisitIds?.length !== 2 || !twentySecondCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__zhuge-liang') || !twentySecondCampState.report?.bonds?.some((bond) => bond.id === 'liu-bei__mi-zhu') || twentySecondCampState.rosterCollection?.total < 8 || twentySecondCampState.rosterCollection?.reserveTrainingCount < 2 || twentySecondCampState.rosterCollection?.reserveTrainingExp < 12 ) { throw new Error(`Expected twenty-second camp to expose Red Cliffs fire dialogue/visit sets, aftermath bonds, and reserve growth: ${JSON.stringify(twentySecondCampState)}`); } await page.screenshot({ path: 'dist/verification-redcliffs-fire-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postFireAttackProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || postFireAttackProgressState.campaignProgress?.totalKnown !== 38 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' ) { throw new Error(`Expected post-Red Cliffs fire progress tab to complete known battles and point toward Jing Province preparation: ${JSON.stringify(postFireAttackProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-redcliffs-fire-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const jingzhouSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jingzhouSortieState?.sortieVisible || !jingzhouSortieState.sortiePlan?.objectiveLine?.includes('형주 남부 진입전') || !jingzhouSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !jingzhouSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || !jingzhouSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !jingzhouSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || !jingzhouSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recruited && unit.recommended) || jingzhouSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang') || jingzhouSortieState.sortieRoster?.length < 8 || jingzhouSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-second camp sortie prep to target Jing Province entry with existing recruited officers and no Ma Liang before victory: ${JSON.stringify(jingzhouSortieState)}`); } assertSortieTacticalRoster(jingzhouSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang' ]); const jingzhouPriorityUnits = ['zhuge-liang', 'guan-yu', 'zhao-yun', 'sun-qian', 'mi-zhu']; for (const unitId of jingzhouPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !jingzhouPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const jingzhouSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jingzhouPriorityUnits.every((unitId) => jingzhouSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || jingzhouSortieReadyState.sortiePlan?.selectedCount !== 6 || jingzhouSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Jing Province entry sortie to deploy all recommended priority officers while preserving six-officer pressure: ${JSON.stringify(jingzhouSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-jingzhou-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-jingzhou-story.png', fullPage: true }); for (let i = 0; i < 40; i += 1) { const enteredTwentyThirdBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-third-battle-jingzhou-south-entry'; }); if (enteredTwentyThirdBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-third-battle.png', fullPage: true }); const twentyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentyThirdEnemies = twentyThirdBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentyThirdAllies = twentyThirdBattleState.units.filter((unit) => unit.faction === 'ally'); const twentyThirdEnemyBehaviors = new Set(twentyThirdEnemies.map((unit) => unit.ai)); if ( twentyThirdBattleState.camera?.mapWidth !== 46 || twentyThirdBattleState.camera?.mapHeight !== 32 || twentyThirdBattleState.victoryConditionLabel !== '형도영 퇴각' || twentyThirdEnemies.length < 17 || !twentyThirdEnemyBehaviors.has('aggressive') || !twentyThirdEnemyBehaviors.has('guard') || !twentyThirdEnemyBehaviors.has('hold') || !twentyThirdEnemies.some((unit) => unit.id === 'jing-south-leader-xing-daorong') || !twentyThirdAllies.some((unit) => unit.id === 'zhuge-liang') || !twentyThirdAllies.some((unit) => unit.id === 'zhao-yun') || !twentyThirdAllies.some((unit) => unit.id === 'sun-qian') || twentyThirdAllies.some((unit) => unit.id === 'ma-liang') ) { throw new Error(`Expected twenty-third battle to use Jing Province south map, Xing Daorong objective, mixed AI, selected allied officers, and no Ma Liang before victory: ${JSON.stringify(twentyThirdBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentyThirdCampState?.campBattleId !== 'twenty-third-battle-jingzhou-south-entry' || twentyThirdCampState.campTitle !== '형주 남부 진입 후 군영' || twentyThirdCampState.availableDialogueIds?.length !== 3 || !twentyThirdCampState.availableDialogueIds.every((id) => id.endsWith('jingzhou-south-entry')) || twentyThirdCampState.availableVisitIds?.length !== 2 || !twentyThirdCampState.campaign?.roster?.some((unit) => unit.id === 'ma-liang') || !twentyThirdCampState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recruited) || twentyThirdCampState.rosterCollection?.total < 9 || twentyThirdCampState.rosterCollection?.reserveTrainingCount < 2 || twentyThirdCampState.rosterCollection?.reserveTrainingExp < 12 ) { throw new Error(`Expected twenty-third camp to recruit Ma Liang and expose Jing Province dialogue/visit sets: ${JSON.stringify(twentyThirdCampState)}`); } await page.screenshot({ path: 'dist/verification-jingzhou-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postJingzhouProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || postJingzhouProgressState.campaignProgress?.totalKnown !== 38 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' ) { throw new Error(`Expected post-Jing Province progress tab to complete the new chapter opening and pause on next commandery preparation: ${JSON.stringify(postJingzhouProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-jingzhou-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const guiyangSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !guiyangSortieState?.sortieVisible || !guiyangSortieState.sortiePlan?.objectiveLine?.includes('계양 설득전') || !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recruited && unit.recommended) || !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji') || guiyangSortieState.sortieRoster?.length < 9 || guiyangSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-third camp sortie prep to target Guiyang with Ma Liang recommended and Yi Ji absent before victory: ${JSON.stringify(guiyangSortieState)}`); } assertSortieTacticalRoster(guiyangSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang' ]); const guiyangPriorityUnits = ['ma-liang', 'zhuge-liang', 'zhao-yun', 'guan-yu', 'sun-qian']; for (const unitId of guiyangPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !guiyangPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const guiyangSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !guiyangPriorityUnits.every((unitId) => guiyangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || guiyangSortieReadyState.sortiePlan?.selectedCount !== 6 || guiyangSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Guiyang sortie to deploy Ma Liang and other recommended officers while preserving six-officer pressure: ${JSON.stringify(guiyangSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-guiyang-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-guiyang-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredTwentyFourthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fourth-battle-guiyang-persuasion'; }); if (enteredTwentyFourthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-fourth-battle.png', fullPage: true }); const twentyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentyFourthEnemies = twentyFourthBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentyFourthAllies = twentyFourthBattleState.units.filter((unit) => unit.faction === 'ally'); const twentyFourthEnemyBehaviors = new Set(twentyFourthEnemies.map((unit) => unit.ai)); if ( twentyFourthBattleState.camera?.mapWidth !== 48 || twentyFourthBattleState.camera?.mapHeight !== 32 || twentyFourthBattleState.victoryConditionLabel !== '조범 항복' || twentyFourthEnemies.length < 17 || !twentyFourthEnemyBehaviors.has('aggressive') || !twentyFourthEnemyBehaviors.has('guard') || !twentyFourthEnemyBehaviors.has('hold') || !twentyFourthEnemies.some((unit) => unit.id === 'guiyang-leader-zhao-fan') || !twentyFourthAllies.some((unit) => unit.id === 'ma-liang') || !twentyFourthAllies.some((unit) => unit.id === 'zhuge-liang') || !twentyFourthAllies.some((unit) => unit.id === 'sun-qian') || twentyFourthAllies.some((unit) => unit.id === 'yi-ji') ) { throw new Error(`Expected twenty-fourth battle to use Guiyang map, Zhao Fan objective, mixed AI, selected allied officers, and no Yi Ji before victory: ${JSON.stringify(twentyFourthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentyFourthCampState?.campBattleId !== 'twenty-fourth-battle-guiyang-persuasion' || twentyFourthCampState.campTitle !== '계양 설득전 후 군영' || twentyFourthCampState.availableDialogueIds?.length !== 3 || !twentyFourthCampState.availableDialogueIds.every((id) => id.endsWith('guiyang')) || twentyFourthCampState.availableVisitIds?.length !== 2 || !twentyFourthCampState.campaign?.roster?.some((unit) => unit.id === 'yi-ji') || !twentyFourthCampState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited) || twentyFourthCampState.rosterCollection?.total < 10 ) { throw new Error(`Expected twenty-fourth camp to recruit Yi Ji and expose Guiyang dialogue/visit sets: ${JSON.stringify(twentyFourthCampState)}`); } await page.screenshot({ path: 'dist/verification-guiyang-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postGuiyangProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || postGuiyangProgressState.campaignProgress?.totalKnown !== 38 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' ) { throw new Error(`Expected post-Guiyang progress tab to complete the new commandery battle and pause on Wuling preparation: ${JSON.stringify(postGuiyangProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-guiyang-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const wulingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !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 === '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) || wulingSortieState.sortieRoster?.some((unit) => unit.id === 'gong-zhi') || wulingSortieState.sortieRoster?.length < 10 || wulingSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-fourth camp sortie prep to target Wuling with Yi Ji recommended and Gong Zhi absent before victory: ${JSON.stringify(wulingSortieState)}`); } assertSortieTacticalRoster(wulingSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji' ]); const wulingPriorityUnits = ['ma-liang', 'yi-ji', 'zhuge-liang', 'zhao-yun', 'zhang-fei']; for (const unitId of wulingPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !wulingPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const wulingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !wulingPriorityUnits.every((unitId) => wulingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || wulingSortieReadyState.sortiePlan?.selectedCount !== 6 || wulingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-wuling-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredTwentyFifthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fifth-battle-wuling-mountain-road'; }); if (enteredTwentyFifthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-fifth-battle.png', fullPage: true }); const twentyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentyFifthEnemies = twentyFifthBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentyFifthAllies = twentyFifthBattleState.units.filter((unit) => unit.faction === 'ally'); const twentyFifthEnemyBehaviors = new Set(twentyFifthEnemies.map((unit) => unit.ai)); if ( twentyFifthBattleState.camera?.mapWidth !== 50 || twentyFifthBattleState.camera?.mapHeight !== 34 || twentyFifthBattleState.victoryConditionLabel !== '김선 퇴각' || twentyFifthEnemies.length < 19 || !twentyFifthEnemyBehaviors.has('aggressive') || !twentyFifthEnemyBehaviors.has('guard') || !twentyFifthEnemyBehaviors.has('hold') || !twentyFifthEnemies.some((unit) => unit.id === 'wuling-leader-jin-xuan') || !twentyFifthAllies.some((unit) => unit.id === 'ma-liang') || !twentyFifthAllies.some((unit) => unit.id === 'yi-ji') || !twentyFifthAllies.some((unit) => unit.id === 'zhao-yun') || twentyFifthAllies.some((unit) => unit.id === 'gong-zhi') ) { throw new Error(`Expected twenty-fifth battle to use Wuling map, Jin Xuan objective, mixed AI, selected allied officers, and no Gong Zhi before victory: ${JSON.stringify(twentyFifthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentyFifthCampState?.campBattleId !== 'twenty-fifth-battle-wuling-mountain-road' || twentyFifthCampState.campTitle !== '무릉 산길 확보 후 군영' || twentyFifthCampState.availableDialogueIds?.length !== 3 || !twentyFifthCampState.availableDialogueIds.every((id) => id.endsWith('wuling')) || twentyFifthCampState.availableVisitIds?.length !== 2 || !twentyFifthCampState.campaign?.roster?.some((unit) => unit.id === 'gong-zhi') || !twentyFifthCampState.sortieRoster?.some((unit) => unit.id === 'gong-zhi' && unit.recruited) || twentyFifthCampState.rosterCollection?.total < 11 ) { throw new Error(`Expected twenty-fifth camp to recruit Gong Zhi and expose Wuling dialogue/visit sets: ${JSON.stringify(twentyFifthCampState)}`); } await page.screenshot({ path: 'dist/verification-wuling-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postWulingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || postWulingProgressState.campaignProgress?.totalKnown !== 38 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' ) { throw new Error(`Expected post-Wuling progress tab to complete the mountain-road battle and pause on Changsha preparation: ${JSON.stringify(postWulingProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-wuling-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const changshaSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !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 === '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) || changshaSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong') || changshaSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan') || changshaSortieState.sortieRoster?.length < 11 || changshaSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-fifth camp sortie prep to target Changsha with Gong Zhi recommended and Huang Zhong/Wei Yan absent before victory: ${JSON.stringify(changshaSortieState)}`); } assertSortieTacticalRoster(changshaSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi' ]); const changshaPriorityUnits = ['gong-zhi', 'yi-ji', '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)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !changshaPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const changshaSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !changshaPriorityUnits.every((unitId) => changshaSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || changshaSortieReadyState.sortiePlan?.selectedCount !== 6 || changshaSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Changsha sortie to deploy Gong Zhi and the veteran-audience priority officers while preserving six-officer pressure: ${JSON.stringify(changshaSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-changsha-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-changsha-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredTwentySixthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-sixth-battle-changsha-veteran'; }); if (enteredTwentySixthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-sixth-battle.png', fullPage: true }); const twentySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentySixthEnemies = twentySixthBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentySixthAllies = twentySixthBattleState.units.filter((unit) => unit.faction === 'ally'); const twentySixthEnemyBehaviors = new Set(twentySixthEnemies.map((unit) => unit.ai)); if ( twentySixthBattleState.camera?.mapWidth !== 52 || twentySixthBattleState.camera?.mapHeight !== 34 || twentySixthBattleState.victoryConditionLabel !== '한현 퇴각' || twentySixthEnemies.length < 18 || !twentySixthEnemyBehaviors.has('aggressive') || !twentySixthEnemyBehaviors.has('guard') || !twentySixthEnemyBehaviors.has('hold') || !twentySixthEnemies.some((unit) => unit.id === 'changsha-leader-han-xuan') || !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 === 'huang-zhong') || twentySixthAllies.some((unit) => unit.id === 'wei-yan') ) { throw new Error(`Expected twenty-sixth battle to use Changsha map, Han Xuan objective, Huang Zhong/Wei Yan as enemy officers, mixed AI, and no Changsha recruits before victory: ${JSON.stringify(twentySixthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentySixthCampState?.campBattleId !== 'twenty-sixth-battle-changsha-veteran' || twentySixthCampState.campTitle !== '장사 노장 대면 후 군영' || twentySixthCampState.availableDialogueIds?.length !== 4 || !twentySixthCampState.availableDialogueIds.every((id) => id.endsWith('changsha')) || twentySixthCampState.availableVisitIds?.length !== 2 || !twentySixthCampState.campaign?.roster?.some((unit) => unit.id === 'huang-zhong') || !twentySixthCampState.campaign?.roster?.some((unit) => unit.id === 'wei-yan') || !twentySixthCampState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited) || !twentySixthCampState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited) || twentySixthCampState.rosterCollection?.total < 13 ) { throw new Error(`Expected twenty-sixth camp to recruit Huang Zhong and Wei Yan and expose Changsha dialogue/visit sets: ${JSON.stringify(twentySixthCampState)}`); } await page.screenshot({ path: 'dist/verification-changsha-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postChangshaProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postChangshaProgressState?.activeTab !== 'progress' || postChangshaProgressState.campaignProgress?.completedKnown !== 26 || postChangshaProgressState.campaignProgress?.totalKnown !== 38 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로' ) { throw new Error(`Expected post-Changsha progress tab to complete the veteran-audience battle and open Yi Province relief road: ${JSON.stringify(postChangshaProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-changsha-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const yizhouSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !yizhouSortieState?.sortieVisible || !yizhouSortieState.sortiePlan?.objectiveLine?.includes('익주 원군로') || !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited && unit.recommended) || !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recommended) || yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong') || yizhouSortieState.sortieRoster?.length < 13 || yizhouSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-sixth camp sortie prep to target Yi Province relief road with Huang Zhong/Wei Yan selectable and Pang Tong absent before victory: ${JSON.stringify(yizhouSortieState)}`); } assertSortieTacticalRoster(yizhouSortieState, [ 'liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan' ]); const yizhouPriorityUnits = ['zhuge-liang', 'huang-zhong', 'wei-yan', 'zhao-yun', 'mi-zhu']; for (const unitId of yizhouPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !yizhouPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const yizhouSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !yizhouPriorityUnits.every((unitId) => yizhouSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || yizhouSortieReadyState.sortiePlan?.selectedCount !== 6 || yizhouSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Yi Province sortie to deploy Zhuge Liang, Huang Zhong, Wei Yan, Zhao Yun, and Mi Zhu while preserving six-officer pressure: ${JSON.stringify(yizhouSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-yizhou-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-yizhou-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredTwentySeventhBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-seventh-battle-yizhou-relief-road'; }); if (enteredTwentySeventhBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-seventh-battle.png', fullPage: true }); const twentySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentySeventhEnemies = twentySeventhBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentySeventhAllies = twentySeventhBattleState.units.filter((unit) => unit.faction === 'ally'); const twentySeventhEnemyBehaviors = new Set(twentySeventhEnemies.map((unit) => unit.ai)); if ( twentySeventhBattleState.camera?.mapWidth !== 54 || twentySeventhBattleState.camera?.mapHeight !== 36 || twentySeventhBattleState.victoryConditionLabel !== '양회 퇴각' || twentySeventhEnemies.length < 19 || !twentySeventhEnemyBehaviors.has('aggressive') || !twentySeventhEnemyBehaviors.has('guard') || !twentySeventhEnemyBehaviors.has('hold') || !twentySeventhEnemies.some((unit) => unit.id === 'yizhou-leader-yang-huai') || !twentySeventhAllies.some((unit) => unit.id === 'huang-zhong') || !twentySeventhAllies.some((unit) => unit.id === 'wei-yan') || twentySeventhAllies.some((unit) => unit.id === 'pang-tong') ) { throw new Error(`Expected twenty-seventh battle to use Yi Province map, Yang Huai objective, Huang Zhong/Wei Yan as selectable allies, mixed AI, and no Pang Tong before victory: ${JSON.stringify(twentySeventhBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentySeventhCampState?.campBattleId !== 'twenty-seventh-battle-yizhou-relief-road' || twentySeventhCampState.campTitle !== '익주 원군로 후 군영' || twentySeventhCampState.availableDialogueIds?.length !== 3 || !twentySeventhCampState.availableDialogueIds.every((id) => id.endsWith('yizhou')) || twentySeventhCampState.availableVisitIds?.length !== 2 || !twentySeventhCampState.campaign?.roster?.some((unit) => unit.id === 'pang-tong') || !twentySeventhCampState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited) || twentySeventhCampState.rosterCollection?.total < 14 ) { throw new Error(`Expected twenty-seventh camp to recruit Pang Tong and expose Yi Province dialogue/visit sets: ${JSON.stringify(twentySeventhCampState)}`); } await page.screenshot({ path: 'dist/verification-yizhou-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postYizhouProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postYizhouProgressState?.activeTab !== 'progress' || postYizhouProgressState.campaignProgress?.completedKnown !== 27 || postYizhouProgressState.campaignProgress?.totalKnown !== 38 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' ) { throw new Error(`Expected post-Yi Province progress tab to complete the relief-road battle and open Fu Pass entry: ${JSON.stringify(postYizhouProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-yizhou-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const fuPassSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fuPassSortieState?.sortieVisible || !fuPassSortieState.sortiePlan?.objectiveLine?.includes('부수관 진입전') || !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || !fuPassSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !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 === 'fa-zheng') || fuPassSortieState.sortieRoster?.length < 14 || fuPassSortieState.sortiePlan?.maxCount !== 6 ) { 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)}`); } assertSortieTacticalRoster(fuPassSortieState, [ 'liu-bei', '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' ]); const fuPassPriorityUnits = ['pang-tong', 'zhuge-liang', 'wei-yan', 'huang-zhong', 'zhao-yun']; for (const unitId of fuPassPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !fuPassPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const fuPassSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !fuPassPriorityUnits.every((unitId) => fuPassSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || fuPassSortieReadyState.sortiePlan?.selectedCount !== 6 || fuPassSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { 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)}`); } await page.screenshot({ path: 'dist/verification-fupass-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-fupass-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredTwentyEighthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-eighth-battle-fu-pass-entry'; }); if (enteredTwentyEighthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-eighth-battle.png', fullPage: true }); const twentyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentyEighthEnemies = twentyEighthBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentyEighthAllies = twentyEighthBattleState.units.filter((unit) => unit.faction === 'ally'); const twentyEighthEnemyBehaviors = new Set(twentyEighthEnemies.map((unit) => unit.ai)); if ( twentyEighthBattleState.camera?.mapWidth !== 56 || twentyEighthBattleState.camera?.mapHeight !== 38 || twentyEighthBattleState.victoryConditionLabel !== '고패 퇴각' || twentyEighthEnemies.length < 20 || !twentyEighthEnemyBehaviors.has('aggressive') || !twentyEighthEnemyBehaviors.has('guard') || !twentyEighthEnemyBehaviors.has('hold') || !twentyEighthEnemies.some((unit) => unit.id === 'fupass-leader-gao-pei') || !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 === '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)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentyEighthCampState?.campBattleId !== 'twenty-eighth-battle-fu-pass-entry' || twentyEighthCampState.campTitle !== '부수관 진입 후 군영' || twentyEighthCampState.availableDialogueIds?.length !== 3 || !twentyEighthCampState.availableDialogueIds.every((id) => id.endsWith('fupass')) || twentyEighthCampState.availableVisitIds?.length !== 2 || !twentyEighthCampState.campaign?.roster?.some((unit) => unit.id === 'fa-zheng') || !twentyEighthCampState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited) || twentyEighthCampState.rosterCollection?.total < 15 ) { throw new Error(`Expected twenty-eighth camp to recruit Fa Zheng and expose Fu Pass dialogue/visit sets: ${JSON.stringify(twentyEighthCampState)}`); } await page.screenshot({ path: 'dist/verification-fupass-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postFuPassProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postFuPassProgressState?.activeTab !== 'progress' || postFuPassProgressState.campaignProgress?.completedKnown !== 28 || postFuPassProgressState.campaignProgress?.totalKnown !== 38 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' ) { throw new Error(`Expected post-Fu Pass progress tab to complete the pass-entry battle and open Luo Castle outer wall: ${JSON.stringify(postFuPassProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-fupass-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const luoSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !luoSortieState?.sortieVisible || !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 === '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) || luoSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi') || luoSortieState.sortieRoster?.length < 15 || luoSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected twenty-eighth camp sortie prep to target Luo Castle with Fa Zheng selectable and Wu Yi absent before victory: ${JSON.stringify(luoSortieState)}`); } assertSortieTacticalRoster(luoSortieState, [ 'liu-bei', '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' ]); const luoPriorityUnits = ['fa-zheng', 'pang-tong', '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)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !luoPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const luoSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !luoPriorityUnits.every((unitId) => luoSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || luoSortieReadyState.sortiePlan?.selectedCount !== 6 || luoSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Luo Castle sortie to deploy Fa Zheng, Pang Tong, 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-luo-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredTwentyNinthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'twenty-ninth-battle-luo-outer-wall'; }); if (enteredTwentyNinthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-twenty-ninth-battle.png', fullPage: true }); const twentyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const twentyNinthEnemies = twentyNinthBattleState.units.filter((unit) => unit.faction === 'enemy'); const twentyNinthAllies = twentyNinthBattleState.units.filter((unit) => unit.faction === 'ally'); const twentyNinthEnemyBehaviors = new Set(twentyNinthEnemies.map((unit) => unit.ai)); if ( twentyNinthBattleState.camera?.mapWidth !== 58 || twentyNinthBattleState.camera?.mapHeight !== 40 || twentyNinthBattleState.victoryConditionLabel !== '장임 퇴각' || twentyNinthEnemies.length < 20 || !twentyNinthEnemyBehaviors.has('aggressive') || !twentyNinthEnemyBehaviors.has('guard') || !twentyNinthEnemyBehaviors.has('hold') || !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 === 'pang-tong') || !twentyNinthAllies.some((unit) => unit.id === 'zhuge-liang') || 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, selected allied officers, mixed AI, and no Wu Yi before victory: ${JSON.stringify(twentyNinthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const twentyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( twentyNinthCampState?.campBattleId !== 'twenty-ninth-battle-luo-outer-wall' || twentyNinthCampState.campTitle !== '낙성 외곽전 후 군영' || twentyNinthCampState.availableDialogueIds?.length !== 3 || !twentyNinthCampState.availableDialogueIds.every((id) => id.endsWith('luo')) || twentyNinthCampState.availableVisitIds?.length !== 2 || !twentyNinthCampState.campaign?.roster?.some((unit) => unit.id === 'wu-yi') || !twentyNinthCampState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited) || twentyNinthCampState.rosterCollection?.total < 16 ) { throw new Error(`Expected twenty-ninth camp to recruit Wu Yi and expose Luo dialogue/visit sets: ${JSON.stringify(twentyNinthCampState)}`); } await page.screenshot({ path: 'dist/verification-luo-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postLuoProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || postLuoProgressState.campaignProgress?.totalKnown !== 38 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' ) { throw new Error(`Expected post-Luo progress tab to complete the outer-wall battle and open Luofeng Slope ambush: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-luo-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const luofengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !luofengSortieState?.sortieVisible || !luofengSortieState.sortiePlan?.objectiveLine?.includes('낙봉파 매복전') || !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) || !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !luofengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || luofengSortieState.sortieRoster?.length < 16 || luofengSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Luo camp sortie prep to target Luofeng with Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong recommended: ${JSON.stringify(luofengSortieState)}`); } assertSortieTacticalRoster(luofengSortieState, [ 'liu-bei', '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' ]); const luofengPriorityUnits = ['pang-tong', 'wu-yi', 'fa-zheng', 'zhao-yun', 'huang-zhong']; for (const unitId of luofengPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !luofengPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const luofengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !luofengPriorityUnits.every((unitId) => luofengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || luofengSortieReadyState.sortiePlan?.selectedCount !== 6 || luofengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Luofeng sortie to deploy Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(luofengSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-luofeng-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-luofeng-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtiethBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush'; }); if (enteredThirtiethBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirtieth-battle.png', fullPage: true }); const thirtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtiethEnemies = thirtiethBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtiethAllies = thirtiethBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtiethEnemyBehaviors = new Set(thirtiethEnemies.map((unit) => unit.ai)); if ( thirtiethBattleState.camera?.mapWidth !== 60 || thirtiethBattleState.camera?.mapHeight !== 42 || thirtiethBattleState.victoryConditionLabel !== '장임 매복 격파' || thirtiethEnemies.length < 23 || !thirtiethEnemyBehaviors.has('aggressive') || !thirtiethEnemyBehaviors.has('guard') || !thirtiethEnemyBehaviors.has('hold') || !thirtiethEnemies.some((unit) => unit.id === 'luofeng-leader-zhang-ren') || !thirtiethAllies.some((unit) => unit.id === 'pang-tong') || !thirtiethAllies.some((unit) => unit.id === 'wu-yi') || !thirtiethAllies.some((unit) => unit.id === 'fa-zheng') ) { throw new Error(`Expected thirtieth battle to use Luofeng map, Zhang Ren ambush objective, selected allied officers, and mixed AI: ${JSON.stringify(thirtiethBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtiethCampState?.campBattleId !== 'thirtieth-battle-luofeng-ambush' || thirtiethCampState.campTitle !== '낙봉파 매복전 후 군영' || thirtiethCampState.availableDialogueIds?.length !== 3 || !thirtiethCampState.availableDialogueIds.every((id) => id.endsWith('luofeng')) || thirtiethCampState.availableVisitIds?.length !== 2 || !thirtiethCampState.campaign?.roster?.some((unit) => unit.id === 'wu-yi') || thirtiethCampState.rosterCollection?.total < 16 ) { throw new Error(`Expected thirtieth camp to preserve Wu Yi and expose Luofeng dialogue/visit sets: ${JSON.stringify(thirtiethCampState)}`); } await page.screenshot({ path: 'dist/verification-luofeng-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postLuofengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postLuofengProgressState?.activeTab !== 'progress' || postLuofengProgressState.campaignProgress?.completedKnown !== 30 || postLuofengProgressState.campaignProgress?.totalKnown !== 38 || postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략' ) { throw new Error(`Expected post-Luofeng progress tab to complete Luofeng and open Luo Castle main gate: ${JSON.stringify(postLuofengProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-luofeng-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const luoMainSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !luoMainSortieState?.sortieVisible || !luoMainSortieState.sortiePlan?.objectiveLine?.includes('낙성 본성 공략') || !luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || !luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) || !luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || !luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan') || luoMainSortieState.sortieRoster?.length < 16 || luoMainSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Luofeng camp sortie prep to target Luo Castle main gate with Pang Tong, Fa Zheng, Wu Yi, Huang Zhong, and Zhang Fei recommended, before Yan Yan joins: ${JSON.stringify(luoMainSortieState)}`); } assertSortieTacticalRoster(luoMainSortieState, [ 'liu-bei', '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' ]); const luoMainPriorityUnits = ['pang-tong', 'fa-zheng', 'wu-yi', 'huang-zhong', 'zhang-fei']; for (const unitId of luoMainPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !luoMainPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const luoMainSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !luoMainPriorityUnits.every((unitId) => luoMainSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || luoMainSortieReadyState.sortiePlan?.selectedCount !== 6 || luoMainSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Luo Castle main sortie to deploy Pang Tong, Fa Zheng, Wu Yi, Huang Zhong, and Zhang Fei while preserving six-officer pressure: ${JSON.stringify(luoMainSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-luo-main-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-luo-main-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtyFirstBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-first-battle-luo-main-gate'; }); if (enteredThirtyFirstBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-first-battle.png', fullPage: true }); const thirtyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtyFirstEnemies = thirtyFirstBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtyFirstAllies = thirtyFirstBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtyFirstEnemyBehaviors = new Set(thirtyFirstEnemies.map((unit) => unit.ai)); if ( thirtyFirstBattleState.camera?.mapWidth !== 62 || thirtyFirstBattleState.camera?.mapHeight !== 44 || thirtyFirstBattleState.victoryConditionLabel !== '장임 본성 격파' || thirtyFirstEnemies.length < 25 || !thirtyFirstEnemyBehaviors.has('aggressive') || !thirtyFirstEnemyBehaviors.has('guard') || !thirtyFirstEnemyBehaviors.has('hold') || !thirtyFirstEnemies.some((unit) => unit.id === 'luo-main-leader-zhang-ren') || !thirtyFirstEnemies.some((unit) => unit.id === 'luo-main-veteran-yan-yan') || !thirtyFirstAllies.some((unit) => unit.id === 'pang-tong') || !thirtyFirstAllies.some((unit) => unit.id === 'fa-zheng') || !thirtyFirstAllies.some((unit) => unit.id === 'wu-yi') || !thirtyFirstAllies.some((unit) => unit.id === 'huang-zhong') || !thirtyFirstAllies.some((unit) => unit.id === 'zhang-fei') || thirtyFirstAllies.some((unit) => unit.id === 'yan-yan') ) { throw new Error(`Expected thirty-first battle to use Luo Castle main map, Zhang Ren objective, Yan Yan enemy officer, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFirstBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtyFirstCampState?.campBattleId !== 'thirty-first-battle-luo-main-gate' || thirtyFirstCampState.campTitle !== '낙성 본성 공략 후 군영' || thirtyFirstCampState.availableDialogueIds?.length !== 3 || !thirtyFirstCampState.availableDialogueIds.every((id) => id.endsWith('luomain')) || thirtyFirstCampState.availableVisitIds?.length !== 2 || !thirtyFirstCampState.campaign?.roster?.some((unit) => unit.id === 'yan-yan') || !thirtyFirstCampState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited) || thirtyFirstCampState.rosterCollection?.total < 17 ) { throw new Error(`Expected thirty-first camp to recruit Yan Yan and expose Luo main dialogue/visit sets: ${JSON.stringify(thirtyFirstCampState)}`); } await page.screenshot({ path: 'dist/verification-luo-main-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postLuoMainProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postLuoMainProgressState?.activeTab !== 'progress' || postLuoMainProgressState.campaignProgress?.completedKnown !== 31 || postLuoMainProgressState.campaignProgress?.totalKnown !== 38 || postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' || postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전' ) { throw new Error(`Expected post-Luo-main progress tab to complete Luo Castle proper and open Mianzhu Gate pressure: ${JSON.stringify(postLuoMainProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-luo-main-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const mianzhuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !mianzhuSortieState?.sortieVisible || !mianzhuSortieState.sortiePlan?.objectiveLine?.includes('면죽관 압박전') || !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited && unit.recommended) || !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) || !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) || !mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan') || mianzhuSortieState.sortieRoster?.length < 17 || mianzhuSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Luo main camp sortie prep to target Mianzhu with Yan Yan selectable and Li Yan absent before victory: ${JSON.stringify(mianzhuSortieState)}`); } assertSortieTacticalRoster(mianzhuSortieState, [ 'liu-bei', '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' ]); const mianzhuPriorityUnits = ['yan-yan', 'fa-zheng', 'wu-yi', 'pang-tong', 'huang-zhong']; for (const unitId of mianzhuPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !mianzhuPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const mianzhuSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !mianzhuPriorityUnits.every((unitId) => mianzhuSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || mianzhuSortieReadyState.sortiePlan?.selectedCount !== 6 || mianzhuSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Mianzhu sortie to deploy Yan Yan, Fa Zheng, Wu Yi, Pang Tong, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(mianzhuSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-mianzhu-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-mianzhu-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtySecondBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-second-battle-mianzhu-gate'; }); if (enteredThirtySecondBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-second-battle.png', fullPage: true }); const thirtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtySecondEnemies = thirtySecondBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtySecondAllies = thirtySecondBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtySecondEnemyBehaviors = new Set(thirtySecondEnemies.map((unit) => unit.ai)); if ( thirtySecondBattleState.camera?.mapWidth !== 64 || thirtySecondBattleState.camera?.mapHeight !== 46 || thirtySecondBattleState.victoryConditionLabel !== '이엄 수비선 격파' || thirtySecondEnemies.length < 25 || !thirtySecondEnemyBehaviors.has('aggressive') || !thirtySecondEnemyBehaviors.has('guard') || !thirtySecondEnemyBehaviors.has('hold') || !thirtySecondEnemies.some((unit) => unit.id === 'chengdu-leader-li-yan') || !thirtySecondEnemies.some((unit) => unit.id === 'chengdu-officer-fei-guan') || !thirtySecondAllies.some((unit) => unit.id === 'yan-yan') || !thirtySecondAllies.some((unit) => unit.id === 'fa-zheng') || !thirtySecondAllies.some((unit) => unit.id === 'wu-yi') || !thirtySecondAllies.some((unit) => unit.id === 'pang-tong') || !thirtySecondAllies.some((unit) => unit.id === 'huang-zhong') || thirtySecondAllies.some((unit) => unit.id === 'li-yan') ) { throw new Error(`Expected thirty-second battle to use Mianzhu map, Li Yan objective, Yan Yan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtySecondBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtySecondCampState?.campBattleId !== 'thirty-second-battle-mianzhu-gate' || thirtySecondCampState.campTitle !== '면죽관 압박전 후 군영' || thirtySecondCampState.availableDialogueIds?.length !== 3 || !thirtySecondCampState.availableDialogueIds.every((id) => id.endsWith('mianzhu')) || thirtySecondCampState.availableVisitIds?.length !== 2 || !thirtySecondCampState.campaign?.roster?.some((unit) => unit.id === 'li-yan') || !thirtySecondCampState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recruited) || thirtySecondCampState.rosterCollection?.total < 18 ) { throw new Error(`Expected thirty-second camp to recruit Li Yan and expose Mianzhu dialogue/visit sets: ${JSON.stringify(thirtySecondCampState)}`); } await page.screenshot({ path: 'dist/verification-mianzhu-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postMianzhuProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postMianzhuProgressState?.activeTab !== 'progress' || postMianzhuProgressState.campaignProgress?.completedKnown !== 32 || postMianzhuProgressState.campaignProgress?.totalKnown !== 38 || postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' || postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전' ) { throw new Error(`Expected post-Mianzhu progress tab to complete Mianzhu pressure and open Chengdu surrender: ${JSON.stringify(postMianzhuProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-mianzhu-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const chengduSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !chengduSortieState?.sortieVisible || !chengduSortieState.sortiePlan?.objectiveLine?.includes('성도 항복 권고전') || !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recruited && unit.recommended) || !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited && unit.recommended) || !chengduSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) || chengduSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan') || chengduSortieState.sortieRoster?.length < 18 || chengduSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Mianzhu camp sortie prep to target Chengdu surrender with Li Yan selectable and Huang Quan absent before victory: ${JSON.stringify(chengduSortieState)}`); } assertSortieTacticalRoster(chengduSortieState, [ 'liu-bei', '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' ]); const chengduPriorityUnits = ['li-yan', 'zhuge-liang', 'fa-zheng', 'yan-yan', 'wu-yi']; for (const unitId of chengduPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !chengduPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const chengduSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !chengduPriorityUnits.every((unitId) => chengduSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || chengduSortieReadyState.sortiePlan?.selectedCount !== 6 || chengduSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Chengdu surrender sortie to deploy Li Yan, Zhuge Liang, Fa Zheng, Yan Yan, and Wu Yi while preserving six-officer pressure: ${JSON.stringify(chengduSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-chengdu-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-chengdu-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtyThirdBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-third-battle-chengdu-surrender'; }); if (enteredThirtyThirdBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-third-battle.png', fullPage: true }); const thirtyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtyThirdEnemies = thirtyThirdBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtyThirdAllies = thirtyThirdBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtyThirdEnemyBehaviors = new Set(thirtyThirdEnemies.map((unit) => unit.ai)); if ( thirtyThirdBattleState.camera?.mapWidth !== 66 || thirtyThirdBattleState.camera?.mapHeight !== 48 || thirtyThirdBattleState.victoryConditionLabel !== '황권 방어선 격파' || thirtyThirdEnemies.length < 26 || !thirtyThirdEnemyBehaviors.has('aggressive') || !thirtyThirdEnemyBehaviors.has('guard') || !thirtyThirdEnemyBehaviors.has('hold') || !thirtyThirdEnemies.some((unit) => unit.id === 'chengdu-final-leader-huang-quan') || !thirtyThirdEnemies.some((unit) => unit.id === 'chengdu-final-officer-a') || !thirtyThirdAllies.some((unit) => unit.id === 'li-yan') || !thirtyThirdAllies.some((unit) => unit.id === 'zhuge-liang') || !thirtyThirdAllies.some((unit) => unit.id === 'fa-zheng') || !thirtyThirdAllies.some((unit) => unit.id === 'yan-yan') || !thirtyThirdAllies.some((unit) => unit.id === 'wu-yi') || thirtyThirdAllies.some((unit) => unit.id === 'huang-quan') ) { throw new Error(`Expected thirty-third battle to use Chengdu surrender map, Huang Quan objective, Li Yan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyThirdBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtyThirdCampState?.campBattleId !== 'thirty-third-battle-chengdu-surrender' || thirtyThirdCampState.campTitle !== '성도 항복 후 군영' || thirtyThirdCampState.availableDialogueIds?.length !== 3 || !thirtyThirdCampState.availableDialogueIds.every((id) => id.endsWith('chengdu')) || thirtyThirdCampState.availableVisitIds?.length !== 2 || !thirtyThirdCampState.campaign?.roster?.some((unit) => unit.id === 'huang-quan') || !thirtyThirdCampState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited) || thirtyThirdCampState.rosterCollection?.total < 19 ) { throw new Error(`Expected thirty-third camp to recruit Huang Quan and expose Chengdu dialogue/visit sets: ${JSON.stringify(thirtyThirdCampState)}`); } await page.screenshot({ path: 'dist/verification-chengdu-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postChengduProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postChengduProgressState?.activeTab !== 'progress' || postChengduProgressState.campaignProgress?.completedKnown !== 33 || postChengduProgressState.campaignProgress?.totalKnown !== 38 || postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전' ) { throw new Error(`Expected post-Chengdu progress tab to complete Yi Province capture and open Jiameng Pass: ${JSON.stringify(postChengduProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-chengdu-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const jiamengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jiamengSortieState?.sortieVisible || !jiamengSortieState.sortiePlan?.objectiveLine?.includes('가맹관 마초 대면전') || !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited && unit.recommended) || !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) || !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao') || jiamengSortieState.sortieRoster?.length < 19 || jiamengSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Chengdu camp sortie prep to target Jiameng Pass with Huang Quan selectable and Ma Chao absent before victory: ${JSON.stringify(jiamengSortieState)}`); } assertSortieTacticalRoster(jiamengSortieState, [ 'liu-bei', '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' ]); const jiamengPriorityUnits = ['huang-quan', 'zhao-yun', 'zhang-fei', 'fa-zheng', 'huang-zhong']; for (const unitId of jiamengPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !jiamengPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const jiamengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jiamengPriorityUnits.every((unitId) => jiamengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || jiamengSortieReadyState.sortiePlan?.selectedCount !== 6 || jiamengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Jiameng Pass sortie to deploy Huang Quan, Zhao Yun, Zhang Fei, Fa Zheng, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(jiamengSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-jiameng-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-jiameng-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtyFourthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fourth-battle-jiameng-pass'; }); if (enteredThirtyFourthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-fourth-battle.png', fullPage: true }); const thirtyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtyFourthEnemies = thirtyFourthBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtyFourthAllies = thirtyFourthBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtyFourthEnemyBehaviors = new Set(thirtyFourthEnemies.map((unit) => unit.ai)); if ( thirtyFourthBattleState.camera?.mapWidth !== 68 || thirtyFourthBattleState.camera?.mapHeight !== 50 || thirtyFourthBattleState.victoryConditionLabel !== '마초 돌격 저지' || thirtyFourthEnemies.length < 22 || !thirtyFourthEnemyBehaviors.has('aggressive') || !thirtyFourthEnemyBehaviors.has('guard') || !thirtyFourthEnemyBehaviors.has('hold') || !thirtyFourthEnemies.some((unit) => unit.id === 'jiameng-leader-ma-chao') || !thirtyFourthEnemies.some((unit) => unit.id === 'jiameng-officer-pang-de') || !thirtyFourthAllies.some((unit) => unit.id === 'huang-quan') || !thirtyFourthAllies.some((unit) => unit.id === 'zhao-yun') || !thirtyFourthAllies.some((unit) => unit.id === 'zhang-fei') || !thirtyFourthAllies.some((unit) => unit.id === 'fa-zheng') || !thirtyFourthAllies.some((unit) => unit.id === 'huang-zhong') || thirtyFourthAllies.some((unit) => unit.id === 'ma-chao') ) { throw new Error(`Expected thirty-fourth battle to use Jiameng map, Ma Chao objective, Huang Quan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFourthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtyFourthCampState?.campBattleId !== 'thirty-fourth-battle-jiameng-pass' || thirtyFourthCampState.campTitle !== '가맹관 대면전 후 군영' || thirtyFourthCampState.availableDialogueIds?.length !== 3 || !thirtyFourthCampState.availableDialogueIds.every((id) => id.endsWith('jiameng')) || thirtyFourthCampState.availableVisitIds?.length !== 2 || !thirtyFourthCampState.campaign?.roster?.some((unit) => unit.id === 'ma-chao') || !thirtyFourthCampState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited) || thirtyFourthCampState.rosterCollection?.total < 20 ) { throw new Error(`Expected thirty-fourth camp to recruit Ma Chao and expose Jiameng dialogue/visit sets: ${JSON.stringify(thirtyFourthCampState)}`); } await page.screenshot({ path: 'dist/verification-jiameng-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postJiamengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postJiamengProgressState?.activeTab !== 'progress' || postJiamengProgressState.campaignProgress?.completedKnown !== 34 || postJiamengProgressState.campaignProgress?.totalKnown !== 38 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' ) { throw new Error(`Expected post-Jiameng progress tab to complete Ma Chao chapter and open Yangping scout battle: ${JSON.stringify(postJiamengProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-jiameng-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const yangpingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !yangpingSortieState?.sortieVisible || !yangpingSortieState.sortiePlan?.objectiveLine?.includes('양평관 정찰전') || !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited && unit.recommended) || !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited && unit.recommended) || !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || !yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai') || yangpingSortieState.sortieRoster?.length < 20 || yangpingSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Jiameng camp sortie prep to target Yangping scout battle with Ma Chao selectable and Ma Dai absent before victory: ${JSON.stringify(yangpingSortieState)}`); } assertSortieTacticalRoster(yangpingSortieState, [ 'liu-bei', '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' ]); const yangpingPriorityUnits = ['ma-chao', 'huang-quan', 'zhuge-liang', 'zhao-yun', 'fa-zheng']; for (const unitId of yangpingPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !yangpingPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const yangpingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !yangpingPriorityUnits.every((unitId) => yangpingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || yangpingSortieReadyState.sortiePlan?.selectedCount !== 6 || yangpingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Yangping sortie to deploy Ma Chao, Huang Quan, Zhuge Liang, Zhao Yun, and Fa Zheng while preserving six-officer pressure: ${JSON.stringify(yangpingSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-yangping-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-yangping-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtyFifthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fifth-battle-yangping-scout'; }); if (enteredThirtyFifthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-fifth-battle.png', fullPage: true }); const thirtyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtyFifthEnemies = thirtyFifthBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtyFifthAllies = thirtyFifthBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtyFifthEnemyBehaviors = new Set(thirtyFifthEnemies.map((unit) => unit.ai)); if ( thirtyFifthBattleState.camera?.mapWidth !== 70 || thirtyFifthBattleState.camera?.mapHeight !== 52 || thirtyFifthBattleState.victoryConditionLabel !== '양평관 선봉 격파' || thirtyFifthEnemies.length < 24 || !thirtyFifthEnemyBehaviors.has('aggressive') || !thirtyFifthEnemyBehaviors.has('guard') || !thirtyFifthEnemyBehaviors.has('hold') || !thirtyFifthEnemies.some((unit) => unit.id === 'yangping-leader-zhang-wei') || !thirtyFifthEnemies.some((unit) => unit.id === 'yangping-officer-yan-pu') || !thirtyFifthAllies.some((unit) => unit.id === 'ma-chao') || !thirtyFifthAllies.some((unit) => unit.id === 'huang-quan') || !thirtyFifthAllies.some((unit) => unit.id === 'zhuge-liang') || !thirtyFifthAllies.some((unit) => unit.id === 'zhao-yun') || !thirtyFifthAllies.some((unit) => unit.id === 'fa-zheng') || thirtyFifthAllies.some((unit) => unit.id === 'ma-dai') ) { throw new Error(`Expected thirty-fifth battle to use Yangping map, Zhang Wei objective, Ma Chao first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFifthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtyFifthCampState?.campBattleId !== 'thirty-fifth-battle-yangping-scout' || thirtyFifthCampState.campTitle !== '양평관 정찰 후 군영' || thirtyFifthCampState.availableDialogueIds?.length !== 3 || !thirtyFifthCampState.availableDialogueIds.every((id) => id.endsWith('yangping')) || thirtyFifthCampState.availableVisitIds?.length !== 2 || !thirtyFifthCampState.campaign?.roster?.some((unit) => unit.id === 'ma-dai') || !thirtyFifthCampState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recruited) || thirtyFifthCampState.rosterCollection?.total < 21 ) { throw new Error(`Expected thirty-fifth camp to recruit Ma Dai and expose Yangping dialogue/visit sets: ${JSON.stringify(thirtyFifthCampState)}`); } await page.screenshot({ path: 'dist/verification-yangping-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postYangpingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postYangpingProgressState?.activeTab !== 'progress' || postYangpingProgressState.campaignProgress?.completedKnown !== 35 || postYangpingProgressState.campaignProgress?.totalKnown !== 38 || postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전' ) { throw new Error(`Expected post-Yangping progress tab to complete Ma Dai chapter and open Dingjun vanguard battle: ${JSON.stringify(postYangpingProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-yangping-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const dingjunSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !dingjunSortieState?.sortieVisible || !dingjunSortieState.sortiePlan?.objectiveLine?.includes('정군산 전초전') || !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) || !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited && unit.recommended) || !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recruited && unit.recommended) || !dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping') || dingjunSortieState.sortieRoster?.length < 21 || dingjunSortieState.sortiePlan?.maxCount !== 6 ) { throw new Error(`Expected Yangping camp sortie prep to target Dingjun vanguard with Ma Dai selectable and Wang Ping absent before victory: ${JSON.stringify(dingjunSortieState)}`); } assertSortieTacticalRoster(dingjunSortieState, [ 'liu-bei', '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' ]); const dingjunPriorityUnits = ['huang-zhong', 'fa-zheng', 'ma-chao', 'ma-dai', 'zhuge-liang']; for (const unitId of dingjunPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !dingjunPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const dingjunSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !dingjunPriorityUnits.every((unitId) => dingjunSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || dingjunSortieReadyState.sortiePlan?.selectedCount !== 6 || dingjunSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 ) { throw new Error(`Expected Dingjun sortie to deploy Huang Zhong, Fa Zheng, Ma Chao, Ma Dai, and Zhuge Liang while preserving six-officer pressure: ${JSON.stringify(dingjunSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-dingjun-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-dingjun-story.png', fullPage: true }); for (let i = 0; i < 50; i += 1) { const enteredThirtySixthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard'; }); if (enteredThirtySixthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-sixth-battle.png', fullPage: true }); const thirtySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtySixthEnemies = thirtySixthBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtySixthAllies = thirtySixthBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtySixthEnemyBehaviors = new Set(thirtySixthEnemies.map((unit) => unit.ai)); if ( thirtySixthBattleState.camera?.mapWidth !== 72 || thirtySixthBattleState.camera?.mapHeight !== 54 || thirtySixthBattleState.victoryConditionLabel !== '하후연 선봉 격파' || thirtySixthEnemies.length < 25 || !thirtySixthEnemyBehaviors.has('aggressive') || !thirtySixthEnemyBehaviors.has('guard') || !thirtySixthEnemyBehaviors.has('hold') || !thirtySixthEnemies.some((unit) => unit.id === 'dingjun-leader-xiahou-yuan') || !thirtySixthEnemies.some((unit) => unit.id === 'dingjun-officer-wang-ping') || !thirtySixthAllies.some((unit) => unit.id === 'huang-zhong') || !thirtySixthAllies.some((unit) => unit.id === 'fa-zheng') || !thirtySixthAllies.some((unit) => unit.id === 'ma-chao') || !thirtySixthAllies.some((unit) => unit.id === 'ma-dai') || !thirtySixthAllies.some((unit) => unit.id === 'zhuge-liang') || thirtySixthAllies.some((unit) => unit.id === 'wang-ping') ) { throw new Error(`Expected thirty-sixth battle to use Dingjun map, Xiahou Yuan objective, Huang Zhong first pressure, selected allied officers, and mixed AI: ${JSON.stringify(thirtySixthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtySixthCampState?.campBattleId !== 'thirty-sixth-battle-dingjun-vanguard' || thirtySixthCampState.campTitle !== '정군산 전초전 후 군영' || thirtySixthCampState.availableDialogueIds?.length !== 3 || !thirtySixthCampState.availableDialogueIds.every((id) => id.endsWith('dingjun')) || thirtySixthCampState.availableVisitIds?.length !== 2 || !thirtySixthCampState.campaign?.roster?.some((unit) => unit.id === 'wang-ping') || !thirtySixthCampState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recruited) || thirtySixthCampState.rosterCollection?.total < 22 ) { throw new Error(`Expected thirty-sixth camp to recruit Wang Ping and expose Dingjun dialogue/visit sets: ${JSON.stringify(thirtySixthCampState)}`); } await page.screenshot({ path: 'dist/verification-dingjun-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postDingjunProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postDingjunProgressState?.activeTab !== 'progress' || postDingjunProgressState.campaignProgress?.completedKnown !== 36 || postDingjunProgressState.campaignProgress?.totalKnown !== 38 || postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' || postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전' ) { throw new Error(`Expected post-Dingjun progress tab to complete Wang Ping chapter and open the Hanzhong decisive battle: ${JSON.stringify(postDingjunProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-dingjun-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const hanzhongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !hanzhongSortieState?.sortieVisible || !hanzhongSortieState.sortiePlan?.objectiveLine?.includes('한중 결전') || hanzhongSortieState.sortiePlan?.maxCount !== 7 || hanzhongSortieState.sortiePlan?.recommendedTotal !== 7 || !hanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recruited && unit.recommended) ) { throw new Error(`Expected Hanzhong decisive sortie prep to use a seven-officer roster and recommend Wang Ping: ${JSON.stringify(hanzhongSortieState)}`); } assertSortieTacticalRoster(hanzhongSortieState, [ 'liu-bei', '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' ]); const hanzhongPriorityUnits = ['huang-zhong', 'fa-zheng', 'wang-ping', 'ma-chao', 'ma-dai', 'zhuge-liang']; for (const unitId of hanzhongPriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !hanzhongPriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const hanzhongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !hanzhongPriorityUnits.every((unitId) => hanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || hanzhongSortieReadyState.sortiePlan?.selectedCount !== 7 || hanzhongSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ) { throw new Error(`Expected Hanzhong decisive sortie to deploy Huang Zhong, Fa Zheng, Wang Ping, Ma Chao, Ma Dai, and Zhuge Liang with seven-officer pressure: ${JSON.stringify(hanzhongSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-hanzhong-decisive-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-hanzhong-decisive-story.png', fullPage: true }); for (let i = 0; i < 55; i += 1) { const enteredThirtySeventhBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-seventh-battle-hanzhong-decisive'; }); if (enteredThirtySeventhBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-seventh-battle.png', fullPage: true }); const thirtySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtySeventhEnemies = thirtySeventhBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtySeventhAllies = thirtySeventhBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtySeventhEnemyBehaviors = new Set(thirtySeventhEnemies.map((unit) => unit.ai)); if ( thirtySeventhBattleState.camera?.mapWidth !== 74 || thirtySeventhBattleState.camera?.mapHeight !== 56 || thirtySeventhBattleState.victoryConditionLabel !== '조조 본진 격파' || thirtySeventhEnemies.length < 30 || !thirtySeventhEnemyBehaviors.has('aggressive') || !thirtySeventhEnemyBehaviors.has('guard') || !thirtySeventhEnemyBehaviors.has('hold') || !thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-leader-cao-cao') || !thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-officer-zhang-he') || !thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-officer-xu-huang') || !thirtySeventhAllies.some((unit) => unit.id === 'huang-zhong') || !thirtySeventhAllies.some((unit) => unit.id === 'fa-zheng') || !thirtySeventhAllies.some((unit) => unit.id === 'wang-ping') || !thirtySeventhAllies.some((unit) => unit.id === 'ma-chao') || !thirtySeventhAllies.some((unit) => unit.id === 'ma-dai') || !thirtySeventhAllies.some((unit) => unit.id === 'zhuge-liang') ) { throw new Error(`Expected thirty-seventh battle to use Hanzhong map, Cao Cao objective, Wang Ping terrain pressure, selected allied officers, and mixed AI: ${JSON.stringify(thirtySeventhBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtySeventhCampState?.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' || thirtySeventhCampState.campTitle !== '한중 결전 후 군영' || thirtySeventhCampState.availableDialogueIds?.length !== 3 || !thirtySeventhCampState.availableDialogueIds.every((id) => id.endsWith('hanzhong-decisive')) || thirtySeventhCampState.availableVisitIds?.length !== 2 || !thirtySeventhCampState.campaign?.roster?.some((unit) => unit.id === 'wang-ping') || thirtySeventhCampState.rosterCollection?.total < 22 ) { throw new Error(`Expected thirty-seventh camp to expose Hanzhong decisive dialogue/visit sets and preserve Wang Ping roster: ${JSON.stringify(thirtySeventhCampState)}`); } await page.screenshot({ path: 'dist/verification-hanzhong-decisive-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postHanzhongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postHanzhongProgressState?.activeTab !== 'progress' || postHanzhongProgressState.campaignProgress?.completedKnown !== 37 || postHanzhongProgressState.campaignProgress?.totalKnown !== 38 || postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비' ) { throw new Error(`Expected post-Hanzhong progress tab to complete the decisive battle and expose the King of Hanzhong council: ${JSON.stringify(postHanzhongProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-hanzhong-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const hanzhongKingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !hanzhongKingSortieState?.sortieVisible || hanzhongKingSortieState.campaign?.step !== 'thirty-seventh-camp' || hanzhongKingSortieState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' ) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-hanzhong-king-council-story.png', fullPage: true }); for (let i = 0; i < 16; i += 1) { const returnedToCamp = await page.evaluate(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); }); if (returnedToCamp) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); }); const hanzhongKingCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( hanzhongKingCampState?.campaign?.step !== 'hanzhong-king-camp' || hanzhongKingCampState.campTitle !== '한중왕 즉위 준비 군영' || hanzhongKingCampState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' || hanzhongKingCampState.availableDialogueIds?.length !== 3 || hanzhongKingCampState.availableVisitIds?.length !== 2 ) { throw new Error(`Expected King of Hanzhong council story to persist a dedicated camp step: ${JSON.stringify(hanzhongKingCampState)}`); } await page.screenshot({ path: 'dist/verification-hanzhong-king-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postKingCouncilProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postKingCouncilProgressState?.activeTab !== 'progress' || postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 || postKingCouncilProgressState.campaignProgress?.totalKnown !== 38 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { throw new Error(`Expected King of Hanzhong council to move the progress tab toward Shu-Han foundation prep: ${JSON.stringify(postKingCouncilProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-king-council-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const shuHanFoundationSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !shuHanFoundationSortieState?.sortieVisible || shuHanFoundationSortieState.campaign?.step !== 'hanzhong-king-camp' || shuHanFoundationSortieState.campTitle !== '한중왕 즉위 준비 군영' ) { 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 page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-shu-han-foundation-story.png', fullPage: true }); for (let i = 0; i < 16; i += 1) { const returnedToCamp = await page.evaluate(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); }); if (returnedToCamp) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); }); const shuHanFoundationCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( shuHanFoundationCampState?.campaign?.step !== 'shu-han-foundation-camp' || shuHanFoundationCampState.campTitle !== '촉한 건국 후 군영' || shuHanFoundationCampState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' || shuHanFoundationCampState.availableDialogueIds?.length !== 3 || shuHanFoundationCampState.availableVisitIds?.length !== 2 ) { throw new Error(`Expected Shu-Han foundation story to persist the foundation camp step: ${JSON.stringify(shuHanFoundationCampState)}`); } await page.screenshot({ path: 'dist/verification-shu-han-foundation-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postFoundationProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || postFoundationProgressState.campaignProgress?.totalKnown !== 38 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { throw new Error(`Expected Shu-Han foundation camp to point the progress tab toward Jing Province defense: ${JSON.stringify(postFoundationProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-foundation-progress.png', fullPage: true }); await page.mouse.click(1120, 38); await page.waitForTimeout(180); const jingDefenseSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jingDefenseSortieState?.sortieVisible || jingDefenseSortieState.campaign?.step !== 'shu-han-foundation-camp' || jingDefenseSortieState.sortiePlan?.objectiveLine !== '형주 방위 전초전 · 여몽 정찰대 격파' || jingDefenseSortieState.sortiePlan?.maxCount !== 7 || jingDefenseSortieState.sortiePlan?.recommendedTotal !== 7 || !jingDefenseSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) ) { throw new Error(`Expected Jing defense sortie prep to use a seven-officer roster and recommend Guan Yu's front: ${JSON.stringify(jingDefenseSortieState)}`); } assertSortieTacticalRoster(jingDefenseSortieState, [ 'liu-bei', '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' ]); const jingDefensePriorityUnits = ['guan-yu', 'zhang-fei', 'ma-liang', 'mi-zhu', 'zhuge-liang', 'zhao-yun']; for (const unitId of jingDefensePriorityUnits) { const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { const removable = currentSortieState.sortieRoster?.find( (unit) => unit.selected && unit.id !== 'liu-bei' && !jingDefensePriorityUnits.includes(unit.id) ); if (removable) { await clickSortieRosterUnit(page, removable.id); } await clickSortieRosterUnit(page, unitId); } } const jingDefenseSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( !jingDefensePriorityUnits.every((unitId) => jingDefenseSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) ) || jingDefenseSortieReadyState.sortiePlan?.selectedCount !== 7 || jingDefenseSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ) { throw new Error(`Expected Jing defense sortie to deploy Guan Yu, Zhang Fei, Ma Liang, Mi Zhu, Zhuge Liang, and Zhao Yun with seven-officer pressure: ${JSON.stringify(jingDefenseSortieReadyState)}`); } await page.screenshot({ path: 'dist/verification-jing-defense-sortie.png', fullPage: true }); await page.mouse.click(1068, 646); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('StoryScene'); }); await page.screenshot({ path: 'dist/verification-jing-defense-story.png', fullPage: true }); for (let i = 0; i < 45; i += 1) { const enteredThirtyEighthBattle = await page.evaluate(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.scene === 'BattleScene' && state?.battleId === 'thirty-eighth-battle-jing-defense'; }); if (enteredThirtyEighthBattle) { break; } await page.keyboard.press('Space'); await page.waitForTimeout(320); } 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'; }); await page.screenshot({ path: 'dist/verification-thirty-eighth-battle.png', fullPage: true }); const thirtyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); const thirtyEighthEnemies = thirtyEighthBattleState.units.filter((unit) => unit.faction === 'enemy'); const thirtyEighthAllies = thirtyEighthBattleState.units.filter((unit) => unit.faction === 'ally'); const thirtyEighthEnemyBehaviors = new Set(thirtyEighthEnemies.map((unit) => unit.ai)); if ( thirtyEighthBattleState.camera?.mapWidth !== 76 || thirtyEighthBattleState.camera?.mapHeight !== 58 || thirtyEighthBattleState.victoryConditionLabel !== '여몽 정찰대 격파' || thirtyEighthEnemies.length < 30 || !thirtyEighthEnemyBehaviors.has('aggressive') || !thirtyEighthEnemyBehaviors.has('guard') || !thirtyEighthEnemyBehaviors.has('hold') || !thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-leader-lu-meng') || !thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-officer-cao-ren') || !thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-officer-lu-xun') || !thirtyEighthAllies.some((unit) => unit.id === 'guan-yu') || !thirtyEighthAllies.some((unit) => unit.id === 'ma-liang') || !thirtyEighthAllies.some((unit) => unit.id === 'mi-zhu') || !thirtyEighthAllies.some((unit) => unit.id === 'zhuge-liang') || !thirtyEighthAllies.some((unit) => unit.id === 'zhao-yun') ) { throw new Error(`Expected thirty-eighth battle to use Jing defense map, Lu Meng objective, selected allied officers, and mixed Wu/Wei AI: ${JSON.stringify(thirtyEighthBattleState)}`); } await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); await page.waitForFunction(() => { const state = window.__HEROS_DEBUG__?.battle(); return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; }); await page.mouse.click(738, 642); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); const thirtyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( thirtyEighthCampState?.campBattleId !== 'thirty-eighth-battle-jing-defense' || thirtyEighthCampState.campTitle !== '형주 방위 전초전 후 군영' || thirtyEighthCampState.availableDialogueIds?.length !== 3 || !thirtyEighthCampState.availableDialogueIds.every((id) => id.endsWith('jing-defense')) || thirtyEighthCampState.availableVisitIds?.length !== 2 || thirtyEighthCampState.rosterCollection?.total < 22 ) { throw new Error(`Expected thirty-eighth camp to expose Jing defense dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(thirtyEighthCampState)}`); } await page.screenshot({ path: 'dist/verification-jing-defense-camp.png', fullPage: true }); await page.mouse.click(966, 38); await page.waitForTimeout(180); const postJingDefenseProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || postJingDefenseProgressState.campaignProgress?.totalKnown !== 38 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '준비 중' ) { throw new Error(`Expected post-Jing defense progress tab to complete the thirty-eighth battle and leave Fan Castle as the next prepared slice: ${JSON.stringify(postJingDefenseProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-jing-defense-progress.png', fullPage: true }); await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); game?.scene.stop('BattleScene'); game?.scene.stop('StoryScene'); game?.scene.start('TitleScene'); }); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('TitleScene') && !activeScenes.includes('CampScene'); }); await page.waitForTimeout(300); await page.mouse.click(962, 310); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; return activeScenes.includes('CampScene'); }); console.log(`Verified title-to-thirty-eighth-battle flow, recruited officer sortie selection, 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 joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, and Jing Province defense vanguard state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { serverProcess.kill(); } } async function ensureLocalServer(url) { if (await canReach(url)) { return undefined; } const parsed = new URL(url); const isLocal = ['localhost', '127.0.0.1', '0.0.0.0'].includes(parsed.hostname); if (!isLocal) { throw new Error(`No server responded at ${url}`); } const stderr = []; const child = spawn(process.execPath, ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', parsed.port || '5173'], { cwd: process.cwd(), env: process.env, stdio: ['ignore', 'pipe', 'pipe'] }); child.stderr.on('data', (chunk) => stderr.push(chunk.toString())); child.stdout.on('data', () => {}); for (let i = 0; i < 80; i += 1) { if (await canReach(url)) { return child; } await delay(250); } child.kill(); throw new Error(`Vite server did not start at ${url}\n${stderr.join('')}`); } async function canReach(url) { try { const response = await fetch(url, { signal: AbortSignal.timeout(1000) }); return response.ok; } catch { return false; } } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function assertSortieTacticalRoster(state, requiredUnitIds) { if (!state?.sortieVisible || !Array.isArray(state.sortieRoster)) { throw new Error(`Expected sortie tactical roster to be exposed: ${JSON.stringify(state)}`); } const missingIds = requiredUnitIds.filter((unitId) => !state.sortieRoster.some((unit) => unit.id === unitId)); if (missingIds.length > 0) { throw new Error(`Expected sortie roster to include ${missingIds.join(', ')}: ${JSON.stringify(state.sortieRoster)}`); } const invalidUnits = state.sortieRoster.filter((unit) => { return ( !unit.classRole || !unit.formationRole || !unit.statLine?.includes('무 ') || !unit.statLine?.includes('지 ') || !unit.equipmentLine?.includes('Lv') || !unit.bondLine?.includes('공명') || !unit.terrainLine?.includes('지형') ); }); if (invalidUnits.length > 0) { throw new Error(`Expected sortie roster to include class, stats, equipment, bond, and terrain advice: ${JSON.stringify(invalidUnits)}`); } const liuBei = state.sortieRoster.find((unit) => unit.id === 'liu-bei'); if (!liuBei?.equipmentLine?.includes('자웅일대검')) { throw new Error(`Expected Liu Bei sortie row to expose named treasure weapon: ${JSON.stringify(liuBei)}`); } if ( !state.sortiePlan || !state.sortiePlan.deploymentLine?.includes('전열') || !state.sortiePlan.objectiveLine || !state.sortiePlan.recommendationLine?.includes('추천') || !Array.isArray(state.sortiePlan.formationSlots) || state.sortiePlan.formationSlots.length < 3 || typeof state.sortiePlan.activeBondCount !== 'number' || typeof state.sortiePlan.terrainScore !== 'number' || typeof state.sortiePlan.recommendedSelectedCount !== 'number' || typeof state.sortiePlan.recommendedTotal !== 'number' ) { throw new Error(`Expected sortie preparation to expose deployment plan, bond count, and terrain score: ${JSON.stringify(state.sortiePlan)}`); } const recommendedUnits = state.sortieRoster.filter((unit) => unit.recommended); if (recommendedUnits.length === 0 || recommendedUnits.some((unit) => !unit.recommendationReason)) { throw new Error(`Expected sortie roster to expose recommended officers with reasons: ${JSON.stringify(state.sortieRoster)}`); } } async function clickSortieRosterUnit(page, unitId) { const 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)}`); } 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)}`); } const denseRoster = state.sortieRoster.length > 12; const rowGap = denseRoster ? 17 : state.sortieRoster.length > 7 ? 30 : state.sortieRoster.length > 5 ? 38 : state.sortieRoster.length > 4 ? 43 : 48; await page.mouse.click(254, 314 + (denseRoster ? 48 : 50) + index * rowGap); await page.waitForTimeout(120); }