3606 lines
176 KiB
JavaScript
3606 lines
176 KiB
JavaScript
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: 'networkidle' });
|
|
await page.evaluate(() => window.localStorage.clear());
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
await page.waitForSelector('canvas');
|
|
await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined);
|
|
await page.waitForFunction(() => window.__HEROS_DEBUG__ !== undefined);
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 !== 29 ||
|
|
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 pause on Luofeng preparation: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-luo-progress.png', fullPage: true });
|
|
|
|
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('TitleScene');
|
|
});
|
|
await page.mouse.click(962, 310);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
console.log(`Verified title-to-twenty-ninth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, and Wu Yi joins, result states, and debug API 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);
|
|
}
|