556 lines
22 KiB
JavaScript
556 lines
22 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)}`);
|
|
}
|
|
|
|
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(918, 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)}`);
|
|
}
|
|
|
|
await page.mouse.click(830, 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) {
|
|
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)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-camp-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(792, 596);
|
|
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(938, 596);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('StoryScene');
|
|
});
|
|
|
|
for (let i = 0; i < 14; 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(220);
|
|
}
|
|
|
|
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(938, 596);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('StoryScene');
|
|
});
|
|
|
|
for (let i = 0; i < 14; 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(220);
|
|
}
|
|
|
|
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.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-third-battle flow, 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));
|
|
}
|