9828 lines
512 KiB
JavaScript
9828 lines
512 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { chromium } from 'playwright';
|
|
|
|
const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/';
|
|
|
|
function unitUsesTexture(state, unitId, textureBase) {
|
|
return state?.units?.some((unit) => unit.id === unitId && unit.textureBase === textureBase && unit.actionTexture === `${textureBase}-actions`);
|
|
}
|
|
|
|
let serverProcess;
|
|
let browser;
|
|
|
|
try {
|
|
serverProcess = await ensureLocalServer(targetUrl);
|
|
|
|
browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
|
|
|
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(() => window.localStorage.clear());
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => document.querySelector('canvas') !== null, undefined, { timeout: 90000 });
|
|
await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined, undefined, { timeout: 90000 });
|
|
await page.waitForFunction(() => window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 });
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('TitleScene');
|
|
}, undefined, { timeout: 90000 });
|
|
|
|
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.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.phase === 'idle' && state?.mapBackgroundReady === true;
|
|
});
|
|
|
|
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 unitTextureLoadState = await page.evaluate(() => {
|
|
const textures = window.__HEROS_GAME__?.textures;
|
|
const battleState = window.__HEROS_DEBUG__?.battle();
|
|
const missingFirstBattleTextures = (battleState?.units ?? [])
|
|
.flatMap((unit) => [unit.textureBase, unit.actionTexture])
|
|
.filter((key, index, keys) => key && keys.indexOf(key) === index)
|
|
.filter((key) => !textures?.exists(key));
|
|
const firstBattleTexturesReady = missingFirstBattleTextures.length <= 0;
|
|
const lateBattleTexturesDeferred = [
|
|
'unit-zhuge-liang-actions',
|
|
'unit-jiang-wei-actions',
|
|
'unit-sima-yi-actions',
|
|
'unit-wang-ping-actions'
|
|
].every((key) => !textures?.exists(key));
|
|
return { firstBattleTexturesReady, lateBattleTexturesDeferred, missingFirstBattleTextures };
|
|
});
|
|
if (!unitTextureLoadState.firstBattleTexturesReady || !unitTextureLoadState.lateBattleTexturesDeferred) {
|
|
throw new Error(`Expected first battle unit textures to load while late campaign textures remain deferred: ${JSON.stringify(unitTextureLoadState)}`);
|
|
}
|
|
|
|
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)}`);
|
|
}
|
|
|
|
const visibleEnemyScreen = await page.evaluate(() => {
|
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
const enemy = state?.units?.find((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
|
|
|
if (!scene || !enemy) {
|
|
throw new Error('Expected enemy and battle scene for enemy threat click test.');
|
|
}
|
|
|
|
scene.centerCameraOnTile(enemy.x, enemy.y);
|
|
return {
|
|
x: scene.tileCenterX(enemy.x),
|
|
y: scene.tileCenterY(enemy.y)
|
|
};
|
|
});
|
|
await page.waitForTimeout(120);
|
|
await page.mouse.click(visibleEnemyScreen.x, visibleEnemyScreen.y);
|
|
await page.waitForTimeout(180);
|
|
|
|
const clickedEnemyThreatState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
if (
|
|
!clickedEnemyThreatState?.enemyMoveMarkerCount ||
|
|
clickedEnemyThreatState.enemyMoveMarkerCount <= 0 ||
|
|
!clickedEnemyThreatState.threatMarkerCount ||
|
|
clickedEnemyThreatState.threatMarkerCount <= 0
|
|
) {
|
|
throw new Error(`Expected clicked enemy movement and attack markers: ${JSON.stringify(clickedEnemyThreatState)}`);
|
|
}
|
|
|
|
const combatMechanicsProbe = clickedEnemyThreatState.combatMechanicsProbe;
|
|
if (
|
|
!combatMechanicsProbe ||
|
|
combatMechanicsProbe.adjacentBondDamageBonus <= 0 ||
|
|
combatMechanicsProbe.sameTargetBondDamageBonus <= 0 ||
|
|
combatMechanicsProbe.farBondDamageBonus !== 0 ||
|
|
combatMechanicsProbe.counterDamage >= combatMechanicsProbe.normalDamage
|
|
) {
|
|
throw new Error(`Expected positional bond bonuses and reduced counter damage: ${JSON.stringify(combatMechanicsProbe)}`);
|
|
}
|
|
|
|
const moveWaitTurnPromptStates = await page.evaluate(() => {
|
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
const lastAlly = state?.units?.find((unit) => unit.id === 'zhang-fei');
|
|
|
|
if (!scene || !state || !lastAlly) {
|
|
throw new Error('Expected Zhang Fei and battle scene for move-wait turn prompt test.');
|
|
}
|
|
|
|
scene.hideTurnEndPrompt();
|
|
scene.clearMarkers();
|
|
scene.hideCommandMenu();
|
|
scene.actedUnitIds = new Set(state.units.filter((unit) => unit.faction === 'ally' && unit.hp > 0 && unit.id !== lastAlly.id).map((unit) => unit.id));
|
|
scene.phase = 'idle';
|
|
scene.selectedUnit = undefined;
|
|
scene.pendingMove = undefined;
|
|
|
|
const liveLastAlly = scene.debugUnitById(lastAlly.id);
|
|
if (!liveLastAlly) {
|
|
throw new Error('Expected live Zhang Fei unit for move-wait turn prompt test.');
|
|
}
|
|
|
|
scene.selectUnit(liveLastAlly);
|
|
scene.moveSelectedUnit(liveLastAlly.x, liveLastAlly.y);
|
|
const postMoveState = window.__HEROS_DEBUG__?.battle();
|
|
scene.completeUnitAction(liveLastAlly, 'wait');
|
|
const postWaitState = window.__HEROS_DEBUG__?.battle();
|
|
return { postMoveState, postWaitState };
|
|
});
|
|
if (
|
|
!moveWaitTurnPromptStates?.postMoveState?.turnPromptVisible ||
|
|
moveWaitTurnPromptStates.postMoveState.turnPromptMode !== 'post-move' ||
|
|
moveWaitTurnPromptStates.postMoveState.phase !== 'command' ||
|
|
moveWaitTurnPromptStates.postMoveState.pendingMove?.unitId !== 'zhang-fei' ||
|
|
moveWaitTurnPromptStates.postMoveState.actedUnitIds?.includes('zhang-fei')
|
|
) {
|
|
throw new Error(`Expected final ally move to show post-move turn prompt: ${JSON.stringify(moveWaitTurnPromptStates)}`);
|
|
}
|
|
if (
|
|
!moveWaitTurnPromptStates.postWaitState?.turnPromptVisible ||
|
|
moveWaitTurnPromptStates.postWaitState.turnPromptMode !== 'turn-end' ||
|
|
moveWaitTurnPromptStates.postWaitState.phase !== 'idle'
|
|
) {
|
|
throw new Error(`Expected move-wait completion to show turn-end prompt: ${JSON.stringify(moveWaitTurnPromptStates)}`);
|
|
}
|
|
|
|
await page.evaluate(() => {
|
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
|
if (!scene) {
|
|
return;
|
|
}
|
|
scene.actedUnitIds.clear();
|
|
scene.selectedUnit = undefined;
|
|
scene.pendingMove = undefined;
|
|
scene.targetingAction = undefined;
|
|
scene.selectedUsable = undefined;
|
|
scene.phase = 'idle';
|
|
scene.clearMarkers();
|
|
scene.hideCommandMenu();
|
|
scene.hideTurnEndPrompt();
|
|
});
|
|
|
|
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 waitForStoryReady(page);
|
|
|
|
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 ||
|
|
secondBattleState.mapTextureKey !== 'battle-map-second' ||
|
|
!secondBattleState.mapTextureReady ||
|
|
!secondBattleState.mapBackgroundReady ||
|
|
secondEnemies.length < 10 ||
|
|
!secondEnemyBehaviors.has('aggressive') ||
|
|
!secondEnemyBehaviors.has('guard') ||
|
|
!secondEnemyBehaviors.has('hold')
|
|
) {
|
|
throw new Error(`Expected loaded 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 waitForStoryReady(page);
|
|
|
|
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 waitForStoryReady(page);
|
|
|
|
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 waitForStoryReady(page);
|
|
|
|
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 waitForStoryReady(page);
|
|
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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('백우선 책략')
|
|
) {
|
|
throw new Error(`Expected eighteenth battle to use Bowang map, Xiahou Dun objective, mixed AI, deployed Zhuge Liang, and non-positional 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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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') ||
|
|
!unitUsesTexture(twentySixthBattleState, 'changsha-veteran-huang-zhong', 'unit-huang-zhong') ||
|
|
!unitUsesTexture(twentySixthBattleState, 'changsha-officer-wei-yan', 'unit-wei-yan') ||
|
|
!unitUsesTexture(twentySixthBattleState, 'gong-zhi', 'unit-gong-zhi') ||
|
|
!unitUsesTexture(twentySixthBattleState, 'yi-ji', 'unit-yi-ji') ||
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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 !== 66 ||
|
|
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 waitForStoryReady(page);
|
|
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') ||
|
|
!unitUsesTexture(twentyNinthBattleState, 'luo-officer-wu-yi', 'unit-wu-yi') ||
|
|
!unitUsesTexture(twentyNinthBattleState, 'fa-zheng', 'unit-fa-zheng') ||
|
|
!unitUsesTexture(twentyNinthBattleState, 'pang-tong', 'unit-pang-tong') ||
|
|
twentyNinthAllies.some((unit) => unit.id === 'wu-yi')
|
|
) {
|
|
throw new Error(`Expected twenty-ninth battle to use Luo Castle map, Zhang Ren objective, Wu Yi as enemy officer, 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 !== 66 ||
|
|
postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
|
postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' ||
|
|
postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전'
|
|
) {
|
|
throw new Error(`Expected post-Luo progress tab to complete the outer-wall battle and open Luofeng Slope ambush: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-luo-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const luofengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!luofengSortieState?.sortieVisible ||
|
|
!luofengSortieState.sortiePlan?.objectiveLine?.includes('낙봉파 매복전') ||
|
|
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) ||
|
|
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) ||
|
|
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
|
|
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
|
|
luofengSortieState.sortieRoster?.length < 16 ||
|
|
luofengSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Luo camp sortie prep to target Luofeng with Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong recommended: ${JSON.stringify(luofengSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(luofengSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi'
|
|
]);
|
|
|
|
const luofengPriorityUnits = ['pang-tong', 'wu-yi', 'fa-zheng', 'zhao-yun', 'huang-zhong'];
|
|
for (const unitId of luofengPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !luofengPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const luofengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!luofengPriorityUnits.every((unitId) =>
|
|
luofengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
luofengSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
luofengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Luofeng sortie to deploy Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(luofengSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-luofeng-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-luofeng-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtiethBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush';
|
|
});
|
|
if (enteredThirtiethBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirtieth-battle.png', fullPage: true });
|
|
|
|
const thirtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtiethEnemies = thirtiethBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtiethAllies = thirtiethBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtiethEnemyBehaviors = new Set(thirtiethEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtiethBattleState.camera?.mapWidth !== 60 ||
|
|
thirtiethBattleState.camera?.mapHeight !== 42 ||
|
|
thirtiethBattleState.victoryConditionLabel !== '장임 매복 격파' ||
|
|
thirtiethEnemies.length < 23 ||
|
|
!thirtiethEnemyBehaviors.has('aggressive') ||
|
|
!thirtiethEnemyBehaviors.has('guard') ||
|
|
!thirtiethEnemyBehaviors.has('hold') ||
|
|
!thirtiethEnemies.some((unit) => unit.id === 'luofeng-leader-zhang-ren') ||
|
|
!thirtiethAllies.some((unit) => unit.id === 'pang-tong') ||
|
|
!thirtiethAllies.some((unit) => unit.id === 'wu-yi') ||
|
|
!thirtiethAllies.some((unit) => unit.id === 'fa-zheng')
|
|
) {
|
|
throw new Error(`Expected thirtieth battle to use Luofeng map, Zhang Ren ambush objective, selected allied officers, and mixed AI: ${JSON.stringify(thirtiethBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtiethCampState?.campBattleId !== 'thirtieth-battle-luofeng-ambush' ||
|
|
thirtiethCampState.campTitle !== '낙봉파 매복전 후 군영' ||
|
|
thirtiethCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtiethCampState.availableDialogueIds.every((id) => id.endsWith('luofeng')) ||
|
|
thirtiethCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtiethCampState.campaign?.roster?.some((unit) => unit.id === 'wu-yi') ||
|
|
thirtiethCampState.rosterCollection?.total < 16
|
|
) {
|
|
throw new Error(`Expected thirtieth camp to preserve Wu Yi and expose Luofeng dialogue/visit sets: ${JSON.stringify(thirtiethCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-luofeng-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postLuofengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postLuofengProgressState?.activeTab !== 'progress' ||
|
|
postLuofengProgressState.campaignProgress?.completedKnown !== 30 ||
|
|
postLuofengProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
|
postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' ||
|
|
postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략'
|
|
) {
|
|
throw new Error(`Expected post-Luofeng progress tab to complete Luofeng and open Luo Castle main gate: ${JSON.stringify(postLuofengProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-luofeng-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const luoMainSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!luoMainSortieState?.sortieVisible ||
|
|
!luoMainSortieState.sortiePlan?.objectiveLine?.includes('낙성 본성 공략') ||
|
|
!luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) ||
|
|
!luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
!luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) ||
|
|
!luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
|
|
!luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) ||
|
|
luoMainSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan') ||
|
|
luoMainSortieState.sortieRoster?.length < 16 ||
|
|
luoMainSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Luofeng camp sortie prep to target Luo Castle main gate with Pang Tong, Fa Zheng, Wu Yi, Huang Zhong, and Zhang Fei recommended, before Yan Yan joins: ${JSON.stringify(luoMainSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(luoMainSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi'
|
|
]);
|
|
|
|
const luoMainPriorityUnits = ['pang-tong', 'fa-zheng', 'wu-yi', 'huang-zhong', 'zhang-fei'];
|
|
for (const unitId of luoMainPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !luoMainPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const luoMainSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!luoMainPriorityUnits.every((unitId) =>
|
|
luoMainSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
luoMainSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
luoMainSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Luo Castle main sortie to deploy Pang Tong, Fa Zheng, Wu Yi, Huang Zhong, and Zhang Fei while preserving six-officer pressure: ${JSON.stringify(luoMainSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-luo-main-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-luo-main-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtyFirstBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-first-battle-luo-main-gate';
|
|
});
|
|
if (enteredThirtyFirstBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-first-battle-luo-main-gate' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-first-battle.png', fullPage: true });
|
|
|
|
const thirtyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtyFirstEnemies = thirtyFirstBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtyFirstAllies = thirtyFirstBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtyFirstEnemyBehaviors = new Set(thirtyFirstEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtyFirstBattleState.camera?.mapWidth !== 62 ||
|
|
thirtyFirstBattleState.camera?.mapHeight !== 44 ||
|
|
thirtyFirstBattleState.victoryConditionLabel !== '장임 본성 격파' ||
|
|
thirtyFirstEnemies.length < 25 ||
|
|
!thirtyFirstEnemyBehaviors.has('aggressive') ||
|
|
!thirtyFirstEnemyBehaviors.has('guard') ||
|
|
!thirtyFirstEnemyBehaviors.has('hold') ||
|
|
!thirtyFirstEnemies.some((unit) => unit.id === 'luo-main-leader-zhang-ren') ||
|
|
!thirtyFirstEnemies.some((unit) => unit.id === 'luo-main-veteran-yan-yan') ||
|
|
!thirtyFirstAllies.some((unit) => unit.id === 'pang-tong') ||
|
|
!thirtyFirstAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtyFirstAllies.some((unit) => unit.id === 'wu-yi') ||
|
|
!thirtyFirstAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!thirtyFirstAllies.some((unit) => unit.id === 'zhang-fei') ||
|
|
!unitUsesTexture(thirtyFirstBattleState, 'luo-main-veteran-yan-yan', 'unit-yan-yan') ||
|
|
!unitUsesTexture(thirtyFirstBattleState, 'pang-tong', 'unit-pang-tong') ||
|
|
!unitUsesTexture(thirtyFirstBattleState, 'fa-zheng', 'unit-fa-zheng') ||
|
|
!unitUsesTexture(thirtyFirstBattleState, 'wu-yi', 'unit-wu-yi') ||
|
|
!unitUsesTexture(thirtyFirstBattleState, 'huang-zhong', 'unit-huang-zhong') ||
|
|
thirtyFirstAllies.some((unit) => unit.id === 'yan-yan')
|
|
) {
|
|
throw new Error(`Expected thirty-first battle to use Luo Castle main map, Zhang Ren objective, Yan Yan enemy officer, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFirstBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtyFirstCampState?.campBattleId !== 'thirty-first-battle-luo-main-gate' ||
|
|
thirtyFirstCampState.campTitle !== '낙성 본성 공략 후 군영' ||
|
|
thirtyFirstCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtyFirstCampState.availableDialogueIds.every((id) => id.endsWith('luomain')) ||
|
|
thirtyFirstCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtyFirstCampState.campaign?.roster?.some((unit) => unit.id === 'yan-yan') ||
|
|
!thirtyFirstCampState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited) ||
|
|
thirtyFirstCampState.rosterCollection?.total < 17
|
|
) {
|
|
throw new Error(`Expected thirty-first camp to recruit Yan Yan and expose Luo main dialogue/visit sets: ${JSON.stringify(thirtyFirstCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-luo-main-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postLuoMainProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postLuoMainProgressState?.activeTab !== 'progress' ||
|
|
postLuoMainProgressState.campaignProgress?.completedKnown !== 31 ||
|
|
postLuoMainProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
|
postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' ||
|
|
postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전'
|
|
) {
|
|
throw new Error(`Expected post-Luo-main progress tab to complete Luo Castle proper and open Mianzhu Gate pressure: ${JSON.stringify(postLuoMainProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-luo-main-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const mianzhuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!mianzhuSortieState?.sortieVisible ||
|
|
!mianzhuSortieState.sortiePlan?.objectiveLine?.includes('면죽관 압박전') ||
|
|
!mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited && unit.recommended) ||
|
|
!mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
!mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) ||
|
|
!mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) ||
|
|
!mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
|
|
mianzhuSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan') ||
|
|
mianzhuSortieState.sortieRoster?.length < 17 ||
|
|
mianzhuSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Luo main camp sortie prep to target Mianzhu with Yan Yan selectable and Li Yan absent before victory: ${JSON.stringify(mianzhuSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(mianzhuSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan'
|
|
]);
|
|
|
|
const mianzhuPriorityUnits = ['yan-yan', 'fa-zheng', 'wu-yi', 'pang-tong', 'huang-zhong'];
|
|
for (const unitId of mianzhuPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !mianzhuPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const mianzhuSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!mianzhuPriorityUnits.every((unitId) =>
|
|
mianzhuSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
mianzhuSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
mianzhuSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Mianzhu sortie to deploy Yan Yan, Fa Zheng, Wu Yi, Pang Tong, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(mianzhuSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-mianzhu-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-mianzhu-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtySecondBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-second-battle-mianzhu-gate';
|
|
});
|
|
if (enteredThirtySecondBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-second-battle-mianzhu-gate' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-second-battle.png', fullPage: true });
|
|
|
|
const thirtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtySecondEnemies = thirtySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtySecondAllies = thirtySecondBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtySecondEnemyBehaviors = new Set(thirtySecondEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtySecondBattleState.camera?.mapWidth !== 64 ||
|
|
thirtySecondBattleState.camera?.mapHeight !== 46 ||
|
|
thirtySecondBattleState.victoryConditionLabel !== '이엄 수비선 격파' ||
|
|
thirtySecondEnemies.length < 25 ||
|
|
!thirtySecondEnemyBehaviors.has('aggressive') ||
|
|
!thirtySecondEnemyBehaviors.has('guard') ||
|
|
!thirtySecondEnemyBehaviors.has('hold') ||
|
|
!thirtySecondEnemies.some((unit) => unit.id === 'chengdu-leader-li-yan') ||
|
|
!thirtySecondEnemies.some((unit) => unit.id === 'chengdu-officer-fei-guan') ||
|
|
!thirtySecondAllies.some((unit) => unit.id === 'yan-yan') ||
|
|
!thirtySecondAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtySecondAllies.some((unit) => unit.id === 'wu-yi') ||
|
|
!thirtySecondAllies.some((unit) => unit.id === 'pang-tong') ||
|
|
!thirtySecondAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
thirtySecondAllies.some((unit) => unit.id === 'li-yan')
|
|
) {
|
|
throw new Error(`Expected thirty-second battle to use Mianzhu map, Li Yan objective, Yan Yan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtySecondBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtySecondCampState?.campBattleId !== 'thirty-second-battle-mianzhu-gate' ||
|
|
thirtySecondCampState.campTitle !== '면죽관 압박전 후 군영' ||
|
|
thirtySecondCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtySecondCampState.availableDialogueIds.every((id) => id.endsWith('mianzhu')) ||
|
|
thirtySecondCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtySecondCampState.campaign?.roster?.some((unit) => unit.id === 'li-yan') ||
|
|
!thirtySecondCampState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recruited) ||
|
|
thirtySecondCampState.rosterCollection?.total < 18
|
|
) {
|
|
throw new Error(`Expected thirty-second camp to recruit Li Yan and expose Mianzhu dialogue/visit sets: ${JSON.stringify(thirtySecondCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-mianzhu-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postMianzhuProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postMianzhuProgressState?.activeTab !== 'progress' ||
|
|
postMianzhuProgressState.campaignProgress?.completedKnown !== 32 ||
|
|
postMianzhuProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
|
postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' ||
|
|
postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전'
|
|
) {
|
|
throw new Error(`Expected post-Mianzhu progress tab to complete Mianzhu pressure and open Chengdu surrender: ${JSON.stringify(postMianzhuProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-mianzhu-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const chengduSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!chengduSortieState?.sortieVisible ||
|
|
!chengduSortieState.sortiePlan?.objectiveLine?.includes('성도 항복 권고전') ||
|
|
!chengduSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recruited && unit.recommended) ||
|
|
!chengduSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) ||
|
|
!chengduSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
!chengduSortieState.sortieRoster?.some((unit) => unit.id === 'yan-yan' && unit.recruited && unit.recommended) ||
|
|
!chengduSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) ||
|
|
chengduSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan') ||
|
|
chengduSortieState.sortieRoster?.length < 18 ||
|
|
chengduSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Mianzhu camp sortie prep to target Chengdu surrender with Li Yan selectable and Huang Quan absent before victory: ${JSON.stringify(chengduSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(chengduSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan'
|
|
]);
|
|
|
|
const chengduPriorityUnits = ['li-yan', 'zhuge-liang', 'fa-zheng', 'yan-yan', 'wu-yi'];
|
|
for (const unitId of chengduPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !chengduPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const chengduSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!chengduPriorityUnits.every((unitId) =>
|
|
chengduSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
chengduSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
chengduSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Chengdu surrender sortie to deploy Li Yan, Zhuge Liang, Fa Zheng, Yan Yan, and Wu Yi while preserving six-officer pressure: ${JSON.stringify(chengduSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-chengdu-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-chengdu-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtyThirdBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-third-battle-chengdu-surrender';
|
|
});
|
|
if (enteredThirtyThirdBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-third-battle-chengdu-surrender' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-third-battle.png', fullPage: true });
|
|
|
|
const thirtyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtyThirdEnemies = thirtyThirdBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtyThirdAllies = thirtyThirdBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtyThirdEnemyBehaviors = new Set(thirtyThirdEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtyThirdBattleState.camera?.mapWidth !== 66 ||
|
|
thirtyThirdBattleState.camera?.mapHeight !== 48 ||
|
|
thirtyThirdBattleState.victoryConditionLabel !== '황권 방어선 격파' ||
|
|
thirtyThirdEnemies.length < 26 ||
|
|
!thirtyThirdEnemyBehaviors.has('aggressive') ||
|
|
!thirtyThirdEnemyBehaviors.has('guard') ||
|
|
!thirtyThirdEnemyBehaviors.has('hold') ||
|
|
!thirtyThirdEnemies.some((unit) => unit.id === 'chengdu-final-leader-huang-quan') ||
|
|
!thirtyThirdEnemies.some((unit) => unit.id === 'chengdu-final-officer-a') ||
|
|
!thirtyThirdAllies.some((unit) => unit.id === 'li-yan') ||
|
|
!thirtyThirdAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!thirtyThirdAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtyThirdAllies.some((unit) => unit.id === 'yan-yan') ||
|
|
!thirtyThirdAllies.some((unit) => unit.id === 'wu-yi') ||
|
|
thirtyThirdAllies.some((unit) => unit.id === 'huang-quan')
|
|
) {
|
|
throw new Error(`Expected thirty-third battle to use Chengdu surrender map, Huang Quan objective, Li Yan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyThirdBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtyThirdCampState?.campBattleId !== 'thirty-third-battle-chengdu-surrender' ||
|
|
thirtyThirdCampState.campTitle !== '성도 항복 후 군영' ||
|
|
thirtyThirdCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtyThirdCampState.availableDialogueIds.every((id) => id.endsWith('chengdu')) ||
|
|
thirtyThirdCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtyThirdCampState.campaign?.roster?.some((unit) => unit.id === 'huang-quan') ||
|
|
!thirtyThirdCampState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited) ||
|
|
thirtyThirdCampState.rosterCollection?.total < 19
|
|
) {
|
|
throw new Error(`Expected thirty-third camp to recruit Huang Quan and expose Chengdu dialogue/visit sets: ${JSON.stringify(thirtyThirdCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-chengdu-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postChengduProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postChengduProgressState?.activeTab !== 'progress' ||
|
|
postChengduProgressState.campaignProgress?.completedKnown !== 33 ||
|
|
postChengduProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
|
|
postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' ||
|
|
postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전'
|
|
) {
|
|
throw new Error(`Expected post-Chengdu progress tab to complete Yi Province capture and open Jiameng Pass: ${JSON.stringify(postChengduProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-chengdu-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const jiamengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jiamengSortieState?.sortieVisible ||
|
|
!jiamengSortieState.sortiePlan?.objectiveLine?.includes('가맹관 마초 대면전') ||
|
|
!jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited && unit.recommended) ||
|
|
!jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
|
|
!jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) ||
|
|
!jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
!jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
|
|
jiamengSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao') ||
|
|
jiamengSortieState.sortieRoster?.length < 19 ||
|
|
jiamengSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Chengdu camp sortie prep to target Jiameng Pass with Huang Quan selectable and Ma Chao absent before victory: ${JSON.stringify(jiamengSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(jiamengSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan'
|
|
]);
|
|
|
|
const jiamengPriorityUnits = ['huang-quan', 'zhao-yun', 'zhang-fei', 'fa-zheng', 'huang-zhong'];
|
|
for (const unitId of jiamengPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !jiamengPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const jiamengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jiamengPriorityUnits.every((unitId) =>
|
|
jiamengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
jiamengSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
jiamengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Jiameng Pass sortie to deploy Huang Quan, Zhao Yun, Zhang Fei, Fa Zheng, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(jiamengSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-jiameng-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-jiameng-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtyFourthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fourth-battle-jiameng-pass';
|
|
});
|
|
if (enteredThirtyFourthBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fourth-battle-jiameng-pass' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-fourth-battle.png', fullPage: true });
|
|
|
|
const thirtyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtyFourthEnemies = thirtyFourthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtyFourthAllies = thirtyFourthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtyFourthEnemyBehaviors = new Set(thirtyFourthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtyFourthBattleState.camera?.mapWidth !== 68 ||
|
|
thirtyFourthBattleState.camera?.mapHeight !== 50 ||
|
|
thirtyFourthBattleState.victoryConditionLabel !== '마초 돌격 저지' ||
|
|
thirtyFourthEnemies.length < 22 ||
|
|
!thirtyFourthEnemyBehaviors.has('aggressive') ||
|
|
!thirtyFourthEnemyBehaviors.has('guard') ||
|
|
!thirtyFourthEnemyBehaviors.has('hold') ||
|
|
!thirtyFourthEnemies.some((unit) => unit.id === 'jiameng-leader-ma-chao') ||
|
|
!thirtyFourthEnemies.some((unit) => unit.id === 'jiameng-officer-pang-de') ||
|
|
!thirtyFourthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!thirtyFourthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!thirtyFourthAllies.some((unit) => unit.id === 'zhang-fei') ||
|
|
!thirtyFourthAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtyFourthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!unitUsesTexture(thirtyFourthBattleState, 'jiameng-leader-ma-chao', 'unit-ma-chao') ||
|
|
!unitUsesTexture(thirtyFourthBattleState, 'huang-quan', 'unit-huang-quan') ||
|
|
!unitUsesTexture(thirtyFourthBattleState, 'fa-zheng', 'unit-fa-zheng') ||
|
|
!unitUsesTexture(thirtyFourthBattleState, 'huang-zhong', 'unit-huang-zhong') ||
|
|
thirtyFourthAllies.some((unit) => unit.id === 'ma-chao')
|
|
) {
|
|
throw new Error(`Expected thirty-fourth battle to use Jiameng map, Ma Chao objective, Huang Quan first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFourthBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtyFourthCampState?.campBattleId !== 'thirty-fourth-battle-jiameng-pass' ||
|
|
thirtyFourthCampState.campTitle !== '가맹관 대면전 후 군영' ||
|
|
thirtyFourthCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtyFourthCampState.availableDialogueIds.every((id) => id.endsWith('jiameng')) ||
|
|
thirtyFourthCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtyFourthCampState.campaign?.roster?.some((unit) => unit.id === 'ma-chao') ||
|
|
!thirtyFourthCampState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited) ||
|
|
thirtyFourthCampState.rosterCollection?.total < 20
|
|
) {
|
|
throw new Error(`Expected thirty-fourth camp to recruit Ma Chao and expose Jiameng dialogue/visit sets: ${JSON.stringify(thirtyFourthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-jiameng-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postJiamengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postJiamengProgressState?.activeTab !== 'progress' ||
|
|
postJiamengProgressState.campaignProgress?.completedKnown !== 34 ||
|
|
postJiamengProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
|
|
postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' ||
|
|
postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전'
|
|
) {
|
|
throw new Error(`Expected post-Jiameng progress tab to complete Ma Chao chapter and open Yangping scout battle: ${JSON.stringify(postJiamengProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-jiameng-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const yangpingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!yangpingSortieState?.sortieVisible ||
|
|
!yangpingSortieState.sortiePlan?.objectiveLine?.includes('양평관 정찰전') ||
|
|
!yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited && unit.recommended) ||
|
|
!yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recruited && unit.recommended) ||
|
|
!yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) ||
|
|
!yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
|
|
!yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
yangpingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai') ||
|
|
yangpingSortieState.sortieRoster?.length < 20 ||
|
|
yangpingSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Jiameng camp sortie prep to target Yangping scout battle with Ma Chao selectable and Ma Dai absent before victory: ${JSON.stringify(yangpingSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(yangpingSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao'
|
|
]);
|
|
|
|
const yangpingPriorityUnits = ['ma-chao', 'huang-quan', 'zhuge-liang', 'zhao-yun', 'fa-zheng'];
|
|
for (const unitId of yangpingPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !yangpingPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const yangpingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!yangpingPriorityUnits.every((unitId) =>
|
|
yangpingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
yangpingSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
yangpingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Yangping sortie to deploy Ma Chao, Huang Quan, Zhuge Liang, Zhao Yun, and Fa Zheng while preserving six-officer pressure: ${JSON.stringify(yangpingSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-yangping-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-yangping-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtyFifthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fifth-battle-yangping-scout';
|
|
});
|
|
if (enteredThirtyFifthBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-fifth-battle-yangping-scout' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-fifth-battle.png', fullPage: true });
|
|
|
|
const thirtyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtyFifthEnemies = thirtyFifthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtyFifthAllies = thirtyFifthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtyFifthEnemyBehaviors = new Set(thirtyFifthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtyFifthBattleState.camera?.mapWidth !== 70 ||
|
|
thirtyFifthBattleState.camera?.mapHeight !== 52 ||
|
|
thirtyFifthBattleState.victoryConditionLabel !== '양평관 선봉 격파' ||
|
|
thirtyFifthEnemies.length < 24 ||
|
|
!thirtyFifthEnemyBehaviors.has('aggressive') ||
|
|
!thirtyFifthEnemyBehaviors.has('guard') ||
|
|
!thirtyFifthEnemyBehaviors.has('hold') ||
|
|
!thirtyFifthEnemies.some((unit) => unit.id === 'yangping-leader-zhang-wei') ||
|
|
!thirtyFifthEnemies.some((unit) => unit.id === 'yangping-officer-yan-pu') ||
|
|
!thirtyFifthAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!thirtyFifthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!thirtyFifthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!thirtyFifthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!thirtyFifthAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
thirtyFifthAllies.some((unit) => unit.id === 'ma-dai')
|
|
) {
|
|
throw new Error(`Expected thirty-fifth battle to use Yangping map, Zhang Wei objective, Ma Chao first sortie, selected allied officers, and mixed AI: ${JSON.stringify(thirtyFifthBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtyFifthCampState?.campBattleId !== 'thirty-fifth-battle-yangping-scout' ||
|
|
thirtyFifthCampState.campTitle !== '양평관 정찰 후 군영' ||
|
|
thirtyFifthCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtyFifthCampState.availableDialogueIds.every((id) => id.endsWith('yangping')) ||
|
|
thirtyFifthCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtyFifthCampState.campaign?.roster?.some((unit) => unit.id === 'ma-dai') ||
|
|
!thirtyFifthCampState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recruited) ||
|
|
thirtyFifthCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected thirty-fifth camp to recruit Ma Dai and expose Yangping dialogue/visit sets: ${JSON.stringify(thirtyFifthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-yangping-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postYangpingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postYangpingProgressState?.activeTab !== 'progress' ||
|
|
postYangpingProgressState.campaignProgress?.completedKnown !== 35 ||
|
|
postYangpingProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
|
|
postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' ||
|
|
postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전'
|
|
) {
|
|
throw new Error(`Expected post-Yangping progress tab to complete Ma Dai chapter and open Dingjun vanguard battle: ${JSON.stringify(postYangpingProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-yangping-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const dingjunSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!dingjunSortieState?.sortieVisible ||
|
|
!dingjunSortieState.sortiePlan?.objectiveLine?.includes('정군산 전초전') ||
|
|
!dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
|
|
!dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
|
!dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'ma-chao' && unit.recruited && unit.recommended) ||
|
|
!dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recruited && unit.recommended) ||
|
|
!dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) ||
|
|
dingjunSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping') ||
|
|
dingjunSortieState.sortieRoster?.length < 21 ||
|
|
dingjunSortieState.sortiePlan?.maxCount !== 6
|
|
) {
|
|
throw new Error(`Expected Yangping camp sortie prep to target Dingjun vanguard with Ma Dai selectable and Wang Ping absent before victory: ${JSON.stringify(dingjunSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(dingjunSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai'
|
|
]);
|
|
|
|
const dingjunPriorityUnits = ['huang-zhong', 'fa-zheng', 'ma-chao', 'ma-dai', 'zhuge-liang'];
|
|
for (const unitId of dingjunPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !dingjunPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const dingjunSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!dingjunPriorityUnits.every((unitId) =>
|
|
dingjunSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
dingjunSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
|
dingjunSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
|
) {
|
|
throw new Error(`Expected Dingjun sortie to deploy Huang Zhong, Fa Zheng, Ma Chao, Ma Dai, and Zhuge Liang while preserving six-officer pressure: ${JSON.stringify(dingjunSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-dingjun-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-dingjun-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredThirtySixthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard';
|
|
});
|
|
if (enteredThirtySixthBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-sixth-battle-dingjun-vanguard' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-sixth-battle.png', fullPage: true });
|
|
|
|
const thirtySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtySixthEnemies = thirtySixthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtySixthAllies = thirtySixthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtySixthEnemyBehaviors = new Set(thirtySixthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtySixthBattleState.camera?.mapWidth !== 72 ||
|
|
thirtySixthBattleState.camera?.mapHeight !== 54 ||
|
|
thirtySixthBattleState.victoryConditionLabel !== '하후연 선봉 격파' ||
|
|
thirtySixthEnemies.length < 25 ||
|
|
!thirtySixthEnemyBehaviors.has('aggressive') ||
|
|
!thirtySixthEnemyBehaviors.has('guard') ||
|
|
!thirtySixthEnemyBehaviors.has('hold') ||
|
|
!thirtySixthEnemies.some((unit) => unit.id === 'dingjun-leader-xiahou-yuan') ||
|
|
!thirtySixthEnemies.some((unit) => unit.id === 'dingjun-officer-wang-ping') ||
|
|
!thirtySixthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!thirtySixthAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtySixthAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!thirtySixthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!thirtySixthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!unitUsesTexture(thirtySixthBattleState, 'dingjun-officer-wang-ping', 'unit-wang-ping') ||
|
|
!unitUsesTexture(thirtySixthBattleState, 'huang-zhong', 'unit-huang-zhong') ||
|
|
!unitUsesTexture(thirtySixthBattleState, 'fa-zheng', 'unit-fa-zheng') ||
|
|
!unitUsesTexture(thirtySixthBattleState, 'ma-chao', 'unit-ma-chao') ||
|
|
!unitUsesTexture(thirtySixthBattleState, 'ma-dai', 'unit-ma-dai') ||
|
|
thirtySixthAllies.some((unit) => unit.id === 'wang-ping')
|
|
) {
|
|
throw new Error(`Expected thirty-sixth battle to use Dingjun map, Xiahou Yuan objective, Huang Zhong first pressure, selected allied officers, and mixed AI: ${JSON.stringify(thirtySixthBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtySixthCampState?.campBattleId !== 'thirty-sixth-battle-dingjun-vanguard' ||
|
|
thirtySixthCampState.campTitle !== '정군산 전초전 후 군영' ||
|
|
thirtySixthCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtySixthCampState.availableDialogueIds.every((id) => id.endsWith('dingjun')) ||
|
|
thirtySixthCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtySixthCampState.campaign?.roster?.some((unit) => unit.id === 'wang-ping') ||
|
|
!thirtySixthCampState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recruited) ||
|
|
thirtySixthCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected thirty-sixth camp to recruit Wang Ping and expose Dingjun dialogue/visit sets: ${JSON.stringify(thirtySixthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-dingjun-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postDingjunProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postDingjunProgressState?.activeTab !== 'progress' ||
|
|
postDingjunProgressState.campaignProgress?.completedKnown !== 36 ||
|
|
postDingjunProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
|
|
postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' ||
|
|
postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전'
|
|
) {
|
|
throw new Error(`Expected post-Dingjun progress tab to complete Wang Ping chapter and open the Hanzhong decisive battle: ${JSON.stringify(postDingjunProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-dingjun-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const hanzhongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanzhongSortieState?.sortieVisible ||
|
|
!hanzhongSortieState.sortiePlan?.objectiveLine?.includes('한중 결전') ||
|
|
hanzhongSortieState.sortiePlan?.maxCount !== 7 ||
|
|
hanzhongSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!hanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recruited && unit.recommended)
|
|
) {
|
|
throw new Error(`Expected Hanzhong decisive sortie prep to use a seven-officer roster and recommend Wang Ping: ${JSON.stringify(hanzhongSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(hanzhongSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const hanzhongPriorityUnits = ['huang-zhong', 'fa-zheng', 'wang-ping', 'ma-chao', 'ma-dai', 'zhuge-liang'];
|
|
for (const unitId of hanzhongPriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !hanzhongPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const hanzhongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanzhongPriorityUnits.every((unitId) =>
|
|
hanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
hanzhongSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
hanzhongSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Hanzhong decisive sortie to deploy Huang Zhong, Fa Zheng, Wang Ping, Ma Chao, Ma Dai, and Zhuge Liang with seven-officer pressure: ${JSON.stringify(hanzhongSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-hanzhong-decisive-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-hanzhong-decisive-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 55; i += 1) {
|
|
const enteredThirtySeventhBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-seventh-battle-hanzhong-decisive';
|
|
});
|
|
if (enteredThirtySeventhBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-seventh-battle-hanzhong-decisive' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-seventh-battle.png', fullPage: true });
|
|
|
|
const thirtySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtySeventhEnemies = thirtySeventhBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtySeventhAllies = thirtySeventhBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtySeventhEnemyBehaviors = new Set(thirtySeventhEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtySeventhBattleState.camera?.mapWidth !== 74 ||
|
|
thirtySeventhBattleState.camera?.mapHeight !== 56 ||
|
|
thirtySeventhBattleState.victoryConditionLabel !== '조조 본진 격파' ||
|
|
thirtySeventhEnemies.length < 30 ||
|
|
!thirtySeventhEnemyBehaviors.has('aggressive') ||
|
|
!thirtySeventhEnemyBehaviors.has('guard') ||
|
|
!thirtySeventhEnemyBehaviors.has('hold') ||
|
|
!thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-leader-cao-cao') ||
|
|
!thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-officer-zhang-he') ||
|
|
!thirtySeventhEnemies.some((unit) => unit.id === 'hanzhong-officer-xu-huang') ||
|
|
!thirtySeventhAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!thirtySeventhAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtySeventhAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!thirtySeventhAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!thirtySeventhAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!thirtySeventhAllies.some((unit) => unit.id === 'zhuge-liang')
|
|
) {
|
|
throw new Error(`Expected thirty-seventh battle to use Hanzhong map, Cao Cao objective, Wang Ping terrain pressure, selected allied officers, and mixed AI: ${JSON.stringify(thirtySeventhBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtySeventhCampState?.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' ||
|
|
thirtySeventhCampState.campTitle !== '한중 결전 후 군영' ||
|
|
thirtySeventhCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtySeventhCampState.availableDialogueIds.every((id) => id.endsWith('hanzhong-decisive')) ||
|
|
thirtySeventhCampState.availableVisitIds?.length !== 2 ||
|
|
!thirtySeventhCampState.campaign?.roster?.some((unit) => unit.id === 'wang-ping') ||
|
|
thirtySeventhCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected thirty-seventh camp to expose Hanzhong decisive dialogue/visit sets and preserve Wang Ping roster: ${JSON.stringify(thirtySeventhCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-hanzhong-decisive-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postHanzhongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postHanzhongProgressState?.activeTab !== 'progress' ||
|
|
postHanzhongProgressState.campaignProgress?.completedKnown !== 37 ||
|
|
postHanzhongProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' ||
|
|
postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
|
|
postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비'
|
|
) {
|
|
throw new Error(`Expected post-Hanzhong progress tab to complete the decisive battle and expose the King of Hanzhong council: ${JSON.stringify(postHanzhongProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-hanzhong-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const hanzhongKingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanzhongKingSortieState?.sortieVisible ||
|
|
hanzhongKingSortieState.campaign?.step !== 'thirty-seventh-camp' ||
|
|
hanzhongKingSortieState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive'
|
|
) {
|
|
throw new Error(`Expected King of Hanzhong council to be available from the post-Hanzhong sortie panel: ${JSON.stringify(hanzhongKingSortieState)}`);
|
|
}
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-hanzhong-king-council-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 16; i += 1) {
|
|
const returnedToCamp = await page.evaluate(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene');
|
|
});
|
|
if (returnedToCamp) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene');
|
|
});
|
|
|
|
const hanzhongKingCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
hanzhongKingCampState?.campaign?.step !== 'hanzhong-king-camp' ||
|
|
hanzhongKingCampState.campTitle !== '한중왕 즉위 준비 군영' ||
|
|
hanzhongKingCampState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' ||
|
|
hanzhongKingCampState.availableDialogueIds?.length !== 3 ||
|
|
hanzhongKingCampState.availableVisitIds?.length !== 2
|
|
) {
|
|
throw new Error(`Expected King of Hanzhong council story to persist a dedicated camp step: ${JSON.stringify(hanzhongKingCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-hanzhong-king-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postKingCouncilProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postKingCouncilProgressState?.activeTab !== 'progress' ||
|
|
postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 ||
|
|
postKingCouncilProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
|
|
postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포'
|
|
) {
|
|
throw new Error(`Expected King of Hanzhong council to move the progress tab toward Shu-Han foundation prep: ${JSON.stringify(postKingCouncilProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-king-council-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const shuHanFoundationSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!shuHanFoundationSortieState?.sortieVisible ||
|
|
shuHanFoundationSortieState.campaign?.step !== 'hanzhong-king-camp' ||
|
|
shuHanFoundationSortieState.campTitle !== '한중왕 즉위 준비 군영'
|
|
) {
|
|
throw new Error(`Expected Shu-Han foundation story to be available from the King of Hanzhong camp: ${JSON.stringify(shuHanFoundationSortieState)}`);
|
|
}
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-shu-han-foundation-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 16; i += 1) {
|
|
const returnedToCamp = await page.evaluate(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene');
|
|
});
|
|
if (returnedToCamp) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene');
|
|
});
|
|
|
|
const shuHanFoundationCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
shuHanFoundationCampState?.campaign?.step !== 'shu-han-foundation-camp' ||
|
|
shuHanFoundationCampState.campTitle !== '촉한 건국 후 군영' ||
|
|
shuHanFoundationCampState.campBattleId !== 'thirty-seventh-battle-hanzhong-decisive' ||
|
|
shuHanFoundationCampState.availableDialogueIds?.length !== 3 ||
|
|
shuHanFoundationCampState.availableVisitIds?.length !== 2
|
|
) {
|
|
throw new Error(`Expected Shu-Han foundation story to persist the foundation camp step: ${JSON.stringify(shuHanFoundationCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-shu-han-foundation-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFoundationProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFoundationProgressState?.activeTab !== 'progress' ||
|
|
postFoundationProgressState.campaignProgress?.completedKnown !== 37 ||
|
|
postFoundationProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' ||
|
|
postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전'
|
|
) {
|
|
throw new Error(`Expected Shu-Han foundation camp to point the progress tab toward Jing Province defense: ${JSON.stringify(postFoundationProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-foundation-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const jingDefenseSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jingDefenseSortieState?.sortieVisible ||
|
|
jingDefenseSortieState.campaign?.step !== 'shu-han-foundation-camp' ||
|
|
jingDefenseSortieState.sortiePlan?.objectiveLine !== '형주 방위 전초전 · 여몽 정찰대 격파' ||
|
|
jingDefenseSortieState.sortiePlan?.maxCount !== 7 ||
|
|
jingDefenseSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!jingDefenseSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended)
|
|
) {
|
|
throw new Error(`Expected Jing defense sortie prep to use a seven-officer roster and recommend Guan Yu's front: ${JSON.stringify(jingDefenseSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(jingDefenseSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const jingDefensePriorityUnits = ['guan-yu', 'zhang-fei', 'ma-liang', 'mi-zhu', 'zhuge-liang', 'zhao-yun'];
|
|
for (const unitId of jingDefensePriorityUnits) {
|
|
const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) {
|
|
const removable = currentSortieState.sortieRoster?.find(
|
|
(unit) => unit.selected && unit.id !== 'liu-bei' && !jingDefensePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const jingDefenseSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jingDefensePriorityUnits.every((unitId) =>
|
|
jingDefenseSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
jingDefenseSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
jingDefenseSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Jing defense sortie to deploy Guan Yu, Zhang Fei, Ma Liang, Mi Zhu, Zhuge Liang, and Zhao Yun with seven-officer pressure: ${JSON.stringify(jingDefenseSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-jing-defense-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-jing-defense-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 45; i += 1) {
|
|
const enteredThirtyEighthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-eighth-battle-jing-defense';
|
|
});
|
|
if (enteredThirtyEighthBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-eighth-battle-jing-defense' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-eighth-battle.png', fullPage: true });
|
|
|
|
const thirtyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtyEighthEnemies = thirtyEighthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtyEighthAllies = thirtyEighthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtyEighthEnemyBehaviors = new Set(thirtyEighthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtyEighthBattleState.camera?.mapWidth !== 76 ||
|
|
thirtyEighthBattleState.camera?.mapHeight !== 58 ||
|
|
thirtyEighthBattleState.victoryConditionLabel !== '여몽 정찰대 격파' ||
|
|
thirtyEighthEnemies.length < 30 ||
|
|
!thirtyEighthEnemyBehaviors.has('aggressive') ||
|
|
!thirtyEighthEnemyBehaviors.has('guard') ||
|
|
!thirtyEighthEnemyBehaviors.has('hold') ||
|
|
!thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-leader-lu-meng') ||
|
|
!thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-officer-cao-ren') ||
|
|
!thirtyEighthEnemies.some((unit) => unit.id === 'jing-defense-officer-lu-xun') ||
|
|
!thirtyEighthAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!thirtyEighthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!thirtyEighthAllies.some((unit) => unit.id === 'mi-zhu') ||
|
|
!thirtyEighthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!thirtyEighthAllies.some((unit) => unit.id === 'zhao-yun')
|
|
) {
|
|
throw new Error(`Expected thirty-eighth battle to use Jing defense map, Lu Meng objective, selected allied officers, and mixed Wu/Wei AI: ${JSON.stringify(thirtyEighthBattleState)}`);
|
|
}
|
|
|
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true;
|
|
});
|
|
|
|
await page.mouse.click(738, 642);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene');
|
|
});
|
|
|
|
const thirtyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtyEighthCampState?.campBattleId !== 'thirty-eighth-battle-jing-defense' ||
|
|
thirtyEighthCampState.campTitle !== '형주 방위 전초전 후 군영' ||
|
|
thirtyEighthCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtyEighthCampState.availableDialogueIds.every((id) => id.endsWith('jing-defense')) ||
|
|
thirtyEighthCampState.availableVisitIds?.length !== 2 ||
|
|
thirtyEighthCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected thirty-eighth camp to expose Jing defense dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(thirtyEighthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-jing-defense-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postJingDefenseProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postJingDefenseProgressState?.activeTab !== 'progress' ||
|
|
postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 ||
|
|
postJingDefenseProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' ||
|
|
postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전'
|
|
) {
|
|
throw new Error(`Expected post-Jing defense progress tab to complete the thirty-eighth battle and point to Fan Castle vanguard: ${JSON.stringify(postJingDefenseProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-jing-defense-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const fanCastleSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fanCastleSortieState?.sortieVisible ||
|
|
fanCastleSortieState.campaign?.step !== 'thirty-eighth-camp' ||
|
|
fanCastleSortieState.sortiePlan?.objectiveLine !== '번성 외곽 압박전 · 방덕 선봉 격파' ||
|
|
fanCastleSortieState.sortiePlan?.maxCount !== 7 ||
|
|
fanCastleSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!fanCastleSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
|
|
) {
|
|
throw new Error(`Expected Fan Castle sortie prep to lock Guan Yu and expose a seven-officer choice: ${JSON.stringify(fanCastleSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(fanCastleSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const fanCastlePriorityUnits = ['zhang-fei', 'zhao-yun', 'huang-zhong', 'fa-zheng', 'ma-liang'];
|
|
for (const unitId of fanCastlePriorityUnits) {
|
|
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.required && !fanCastlePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const fanCastleSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fanCastlePriorityUnits.every((unitId) =>
|
|
fanCastleSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!fanCastleSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!fanCastleSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
fanCastleSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
fanCastleSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Fan Castle sortie to deploy Liu Bei, Guan Yu, Zhang Fei, Zhao Yun, Huang Zhong, Fa Zheng, and Ma Liang: ${JSON.stringify(fanCastleSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fan-castle-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fan-castle-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 45; i += 1) {
|
|
const enteredThirtyNinthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-ninth-battle-fan-castle-vanguard';
|
|
});
|
|
if (enteredThirtyNinthBattle) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'thirty-ninth-battle-fan-castle-vanguard' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-thirty-ninth-battle.png', fullPage: true });
|
|
|
|
const thirtyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const thirtyNinthEnemies = thirtyNinthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const thirtyNinthAllies = thirtyNinthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const thirtyNinthEnemyBehaviors = new Set(thirtyNinthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
thirtyNinthBattleState.camera?.mapWidth !== 78 ||
|
|
thirtyNinthBattleState.camera?.mapHeight !== 60 ||
|
|
thirtyNinthBattleState.victoryConditionLabel !== '방덕 선봉 격파' ||
|
|
thirtyNinthEnemies.length < 30 ||
|
|
!thirtyNinthEnemyBehaviors.has('aggressive') ||
|
|
!thirtyNinthEnemyBehaviors.has('guard') ||
|
|
!thirtyNinthEnemyBehaviors.has('hold') ||
|
|
!thirtyNinthEnemies.some((unit) => unit.id === 'fan-castle-leader-pang-de') ||
|
|
!thirtyNinthEnemies.some((unit) => unit.id === 'fan-castle-officer-cao-ren') ||
|
|
!thirtyNinthEnemies.some((unit) => unit.id === 'fan-castle-officer-yu-jin') ||
|
|
!thirtyNinthAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!thirtyNinthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!thirtyNinthAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!thirtyNinthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!thirtyNinthAllies.some((unit) => unit.id === 'zhao-yun')
|
|
) {
|
|
throw new Error(`Expected thirty-ninth battle to use Fan Castle map, Pang De objective, selected allied officers, and mixed castle AI: ${JSON.stringify(thirtyNinthBattleState)}`);
|
|
}
|
|
|
|
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 thirtyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
thirtyNinthCampState?.campBattleId !== 'thirty-ninth-battle-fan-castle-vanguard' ||
|
|
thirtyNinthCampState.campTitle !== '번성 외곽 압박전 후 군영' ||
|
|
thirtyNinthCampState.availableDialogueIds?.length !== 3 ||
|
|
!thirtyNinthCampState.availableDialogueIds.every((id) => id.endsWith('fan-castle')) ||
|
|
thirtyNinthCampState.availableVisitIds?.length !== 2 ||
|
|
thirtyNinthCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected thirty-ninth camp to expose Fan Castle dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(thirtyNinthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fan-castle-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFanCastleProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFanCastleProgressState?.activeTab !== 'progress' ||
|
|
postFanCastleProgressState.campaignProgress?.completedKnown !== 39 ||
|
|
postFanCastleProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' ||
|
|
postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공'
|
|
) {
|
|
throw new Error(`Expected post-Fan Castle progress tab to complete the thirty-ninth battle and point to Han River flood: ${JSON.stringify(postFanCastleProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fan-castle-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const hanRiverSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanRiverSortieState?.sortieVisible ||
|
|
hanRiverSortieState.campaign?.step !== 'thirty-ninth-camp' ||
|
|
hanRiverSortieState.sortiePlan?.objectiveLine !== '한수 수공 · 우금 본대 격파' ||
|
|
hanRiverSortieState.sortiePlan?.maxCount !== 7 ||
|
|
hanRiverSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!hanRiverSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
|
|
) {
|
|
throw new Error(`Expected Han River sortie prep to lock Guan Yu and expose a seven-officer water-control choice: ${JSON.stringify(hanRiverSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(hanRiverSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const hanRiverPriorityUnits = ['fa-zheng', 'ma-liang', 'huang-quan', 'zhao-yun', 'ma-chao'];
|
|
for (const unitId of hanRiverPriorityUnits) {
|
|
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.required && !hanRiverPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const hanRiverSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanRiverPriorityUnits.every((unitId) =>
|
|
hanRiverSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!hanRiverSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!hanRiverSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
hanRiverSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
hanRiverSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Han River sortie to deploy Liu Bei, Guan Yu, Fa Zheng, Ma Liang, Huang Quan, Zhao Yun, and Ma Chao: ${JSON.stringify(hanRiverSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-han-river-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-han-river-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 45; i += 1) {
|
|
const enteredFortiethBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fortieth-battle-han-river-flood';
|
|
});
|
|
if (enteredFortiethBattle) {
|
|
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 === 'fortieth-battle-han-river-flood' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fortieth-battle.png', fullPage: true });
|
|
|
|
const fortiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortiethEnemies = fortiethBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortiethAllies = fortiethBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortiethEnemyBehaviors = new Set(fortiethEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortiethBattleState.camera?.mapWidth !== 80 ||
|
|
fortiethBattleState.camera?.mapHeight !== 62 ||
|
|
fortiethBattleState.victoryConditionLabel !== '우금 본대 격파' ||
|
|
fortiethEnemies.length < 30 ||
|
|
!fortiethEnemyBehaviors.has('aggressive') ||
|
|
!fortiethEnemyBehaviors.has('guard') ||
|
|
!fortiethEnemyBehaviors.has('hold') ||
|
|
!fortiethEnemies.some((unit) => unit.id === 'han-river-leader-yu-jin') ||
|
|
!fortiethEnemies.some((unit) => unit.id === 'han-river-officer-pang-de') ||
|
|
!fortiethEnemies.some((unit) => unit.id === 'han-river-officer-cao-ren') ||
|
|
!fortiethAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!fortiethAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!fortiethAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortiethAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortiethAllies.some((unit) => unit.id === 'zhao-yun')
|
|
) {
|
|
throw new Error(`Expected fortieth battle to use Han River flood map, Yu Jin objective, selected allied officers, and mixed flood AI: ${JSON.stringify(fortiethBattleState)}`);
|
|
}
|
|
|
|
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 fortiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortiethCampState?.campBattleId !== 'fortieth-battle-han-river-flood' ||
|
|
fortiethCampState.campTitle !== '한수 수공 후 군영' ||
|
|
fortiethCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortiethCampState.availableDialogueIds.every((id) => id.endsWith('han-river')) ||
|
|
fortiethCampState.availableVisitIds?.length !== 2 ||
|
|
!fortiethCampState.availableVisitIds.every((id) => id.includes('han-river')) ||
|
|
fortiethCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected fortieth camp to expose Han River dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortiethCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-han-river-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postHanRiverProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postHanRiverProgressState?.activeTab !== 'progress' ||
|
|
postHanRiverProgressState.campaignProgress?.completedKnown !== 40 ||
|
|
postHanRiverProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' ||
|
|
postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전'
|
|
) {
|
|
throw new Error(`Expected post-Han River progress tab to complete the fortieth battle and point to Fan Castle siege: ${JSON.stringify(postHanRiverProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-han-river-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const fanSiegeSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fanSiegeSortieState?.sortieVisible ||
|
|
fanSiegeSortieState.campaign?.step !== 'fortieth-camp' ||
|
|
fanSiegeSortieState.sortiePlan?.objectiveLine !== '번성 공성전 · 조인 성문 방어선 격파' ||
|
|
fanSiegeSortieState.sortiePlan?.maxCount !== 7 ||
|
|
fanSiegeSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!fanSiegeSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
|
|
) {
|
|
throw new Error(`Expected Fan Castle siege sortie prep to lock Guan Yu and expose a seven-officer castle choice: ${JSON.stringify(fanSiegeSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(fanSiegeSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const fanSiegePriorityUnits = ['huang-zhong', 'ma-liang', 'fa-zheng', 'zhao-yun', 'ma-chao'];
|
|
for (const unitId of fanSiegePriorityUnits) {
|
|
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.required && !fanSiegePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const fanSiegeSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fanSiegePriorityUnits.every((unitId) =>
|
|
fanSiegeSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!fanSiegeSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!fanSiegeSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
fanSiegeSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
fanSiegeSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Fan Castle siege sortie to deploy Liu Bei, Guan Yu, Huang Zhong, Ma Liang, Fa Zheng, Zhao Yun, and Ma Chao: ${JSON.stringify(fanSiegeSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fan-siege-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fan-siege-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 45; i += 1) {
|
|
const enteredFortyFirstBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-first-battle-fan-castle-siege';
|
|
});
|
|
if (enteredFortyFirstBattle) {
|
|
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 === 'forty-first-battle-fan-castle-siege' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-first-battle.png', fullPage: true });
|
|
|
|
const fortyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortyFirstEnemies = fortyFirstBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortyFirstAllies = fortyFirstBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortyFirstEnemyBehaviors = new Set(fortyFirstEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortyFirstBattleState.camera?.mapWidth !== 82 ||
|
|
fortyFirstBattleState.camera?.mapHeight !== 64 ||
|
|
fortyFirstBattleState.victoryConditionLabel !== '조인 성문 방어선 격파' ||
|
|
fortyFirstEnemies.length < 35 ||
|
|
!fortyFirstEnemyBehaviors.has('aggressive') ||
|
|
!fortyFirstEnemyBehaviors.has('guard') ||
|
|
!fortyFirstEnemyBehaviors.has('hold') ||
|
|
!fortyFirstEnemies.some((unit) => unit.id === 'fan-siege-leader-cao-ren') ||
|
|
!fortyFirstEnemies.some((unit) => unit.id === 'fan-siege-officer-pang-de') ||
|
|
!fortyFirstEnemies.some((unit) => unit.id === 'fan-siege-officer-lu-meng') ||
|
|
!fortyFirstAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!fortyFirstAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!fortyFirstAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortyFirstAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!fortyFirstAllies.some((unit) => unit.id === 'zhao-yun')
|
|
) {
|
|
throw new Error(`Expected forty-first battle to use Fan Castle siege map, Cao Ren objective, selected allied officers, and mixed siege AI: ${JSON.stringify(fortyFirstBattleState)}`);
|
|
}
|
|
|
|
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 fortyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortyFirstCampState?.campBattleId !== 'forty-first-battle-fan-castle-siege' ||
|
|
fortyFirstCampState.campTitle !== '번성 공성전 후 군영' ||
|
|
fortyFirstCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortyFirstCampState.availableDialogueIds.every((id) => id.endsWith('fan-siege')) ||
|
|
fortyFirstCampState.availableVisitIds?.length !== 2 ||
|
|
!fortyFirstCampState.availableVisitIds.every((id) => id.includes('fan-siege')) ||
|
|
fortyFirstCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected forty-first camp to expose Fan Castle siege dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyFirstCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fan-siege-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFanSiegeProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFanSiegeProgressState?.activeTab !== 'progress' ||
|
|
postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 ||
|
|
postFanSiegeProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' ||
|
|
postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전'
|
|
) {
|
|
throw new Error(`Expected post-Fan Castle siege progress tab to complete the forty-first battle and point to Jing rear crisis: ${JSON.stringify(postFanSiegeProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fan-siege-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const jingRearSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jingRearSortieState?.sortieVisible ||
|
|
jingRearSortieState.campaign?.step !== 'forty-first-camp' ||
|
|
jingRearSortieState.sortiePlan?.objectiveLine !== '강릉 나루 경계전 · 여몽 정찰대 격퇴' ||
|
|
jingRearSortieState.sortiePlan?.maxCount !== 7 ||
|
|
jingRearSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!jingRearSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
|
|
) {
|
|
throw new Error(`Expected Jing rear sortie prep to lock Guan Yu and expose a seven-officer rear-guard choice: ${JSON.stringify(jingRearSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(jingRearSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const jingRearPriorityUnits = ['ma-liang', 'mi-zhu', 'zhao-yun', 'huang-quan', 'wang-ping'];
|
|
for (const unitId of jingRearPriorityUnits) {
|
|
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.required && !jingRearPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
await clickReserveTrainingFocus(page, 'bond-practice');
|
|
|
|
const jingRearSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jingRearPriorityUnits.every((unitId) =>
|
|
jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!jingRearSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
jingRearSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
jingRearSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
jingRearSortieReadyState.reserveTrainingFocus?.id !== 'bond-practice' ||
|
|
!jingRearSortieReadyState.sortiePlan?.reserveTrainingLine?.includes('공명 합숙') ||
|
|
jingRearSortieReadyState.sortiePlan?.reserveTrainingBondPreview !== 4
|
|
) {
|
|
throw new Error(`Expected Jing rear sortie to deploy Liu Bei, Guan Yu, Ma Liang, Mi Zhu, Zhao Yun, Huang Quan, Wang Ping, and bond-focused reserve drill: ${JSON.stringify(jingRearSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-jing-rear-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-jing-rear-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 45; i += 1) {
|
|
const enteredFortySecondBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-second-battle-jing-rear-crisis';
|
|
});
|
|
if (enteredFortySecondBattle) {
|
|
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 === 'forty-second-battle-jing-rear-crisis' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-second-battle.png', fullPage: true });
|
|
|
|
const fortySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortySecondEnemies = fortySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortySecondAllies = fortySecondBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortySecondEnemyBehaviors = new Set(fortySecondEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortySecondBattleState.camera?.mapWidth !== 84 ||
|
|
fortySecondBattleState.camera?.mapHeight !== 66 ||
|
|
fortySecondBattleState.victoryConditionLabel !== '여몽 정찰대 격퇴' ||
|
|
fortySecondEnemies.length < 35 ||
|
|
!fortySecondEnemyBehaviors.has('aggressive') ||
|
|
!fortySecondEnemyBehaviors.has('guard') ||
|
|
!fortySecondEnemyBehaviors.has('hold') ||
|
|
!fortySecondEnemies.some((unit) => unit.id === 'jing-rear-leader-lu-meng') ||
|
|
!fortySecondEnemies.some((unit) => unit.id === 'jing-rear-officer-lu-xun') ||
|
|
!fortySecondEnemies.some((unit) => unit.id === 'jing-rear-officer-mi-fang') ||
|
|
!fortySecondAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!fortySecondAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortySecondAllies.some((unit) => unit.id === 'mi-zhu') ||
|
|
!fortySecondAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortySecondAllies.some((unit) => unit.id === 'wang-ping')
|
|
) {
|
|
throw new Error(`Expected forty-second battle to use Jing rear map, Lu Meng objective, selected rear-guard officers, and mixed Wu AI: ${JSON.stringify(fortySecondBattleState)}`);
|
|
}
|
|
|
|
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 fortySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortySecondCampState?.campBattleId !== 'forty-second-battle-jing-rear-crisis' ||
|
|
fortySecondCampState.campTitle !== '강릉 나루 경계전 후 군영' ||
|
|
fortySecondCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortySecondCampState.availableDialogueIds.every((id) => id.endsWith('jing-rear')) ||
|
|
fortySecondCampState.availableVisitIds?.length !== 2 ||
|
|
!fortySecondCampState.availableVisitIds.every((id) => id.includes('jing-rear')) ||
|
|
fortySecondCampState.rosterCollection?.total < 22 ||
|
|
fortySecondCampState.campaign?.reserveTrainingFocus !== 'bond-practice' ||
|
|
!fortySecondCampState.reserveTrainingAwards?.some(
|
|
(entry) => entry.focusId === 'bond-practice' && entry.focusLabel === '공명 합숙' && entry.bondExpGained > 0
|
|
)
|
|
) {
|
|
throw new Error(`Expected forty-second camp to expose Jing rear dialogue/visit sets, preserve the full officer roster, and record bond-focused reserve training: ${JSON.stringify(fortySecondCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-jing-rear-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postJingRearProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postJingRearProgressState?.activeTab !== 'progress' ||
|
|
postJingRearProgressState.campaignProgress?.completedKnown !== 42 ||
|
|
postJingRearProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' ||
|
|
postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고'
|
|
) {
|
|
throw new Error(`Expected post-Jing rear progress tab to complete the forty-second battle and point to Gongan collapse: ${JSON.stringify(postJingRearProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-jing-rear-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const gonganSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!gonganSortieState?.sortieVisible ||
|
|
gonganSortieState.campaign?.step !== 'forty-second-camp' ||
|
|
gonganSortieState.sortiePlan?.objectiveLine !== '공안 성문 변고 · 여몽 본대 저지' ||
|
|
gonganSortieState.sortiePlan?.maxCount !== 7 ||
|
|
gonganSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!gonganSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required)
|
|
) {
|
|
throw new Error(`Expected Gongan collapse sortie prep to lock Guan Yu and expose a seven-officer rear-collapse choice: ${JSON.stringify(gonganSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(gonganSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const gonganPriorityUnits = ['ma-liang', 'mi-zhu', 'huang-quan', 'wang-ping', 'zhao-yun'];
|
|
for (const unitId of gonganPriorityUnits) {
|
|
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.required && !gonganPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const gonganSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!gonganPriorityUnits.every((unitId) =>
|
|
gonganSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!gonganSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!gonganSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
gonganSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
gonganSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Gongan collapse sortie to deploy Liu Bei, Guan Yu, Ma Liang, Mi Zhu, Huang Quan, Wang Ping, and Zhao Yun: ${JSON.stringify(gonganSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-gongan-collapse-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-gongan-collapse-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 45; i += 1) {
|
|
const enteredFortyThirdBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-third-battle-gongan-collapse';
|
|
});
|
|
if (enteredFortyThirdBattle) {
|
|
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 === 'forty-third-battle-gongan-collapse' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-third-battle.png', fullPage: true });
|
|
|
|
const fortyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortyThirdEnemies = fortyThirdBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortyThirdAllies = fortyThirdBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortyThirdEnemyBehaviors = new Set(fortyThirdEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortyThirdBattleState.camera?.mapWidth !== 86 ||
|
|
fortyThirdBattleState.camera?.mapHeight !== 68 ||
|
|
fortyThirdBattleState.victoryConditionLabel !== '여몽 본대 저지' ||
|
|
fortyThirdEnemies.length < 37 ||
|
|
!fortyThirdEnemyBehaviors.has('aggressive') ||
|
|
!fortyThirdEnemyBehaviors.has('guard') ||
|
|
!fortyThirdEnemyBehaviors.has('hold') ||
|
|
!fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-leader-lu-meng') ||
|
|
!fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-officer-lu-xun') ||
|
|
!fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-officer-fu-shiren') ||
|
|
!fortyThirdEnemies.some((unit) => unit.id === 'jing-collapse-officer-mi-fang') ||
|
|
!fortyThirdAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!fortyThirdAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortyThirdAllies.some((unit) => unit.id === 'mi-zhu') ||
|
|
!fortyThirdAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortyThirdAllies.some((unit) => unit.id === 'wang-ping')
|
|
) {
|
|
throw new Error(`Expected forty-third battle to use Gongan collapse map, Lu Meng objective, selected rear-collapse officers, and mixed Wu AI: ${JSON.stringify(fortyThirdBattleState)}`);
|
|
}
|
|
|
|
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 fortyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortyThirdCampState?.campBattleId !== 'forty-third-battle-gongan-collapse' ||
|
|
fortyThirdCampState.campTitle !== '공안 성문 변고 후 군영' ||
|
|
fortyThirdCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortyThirdCampState.availableDialogueIds.every((id) => id.includes('gongan-collapse')) ||
|
|
fortyThirdCampState.availableVisitIds?.length !== 2 ||
|
|
!fortyThirdCampState.availableVisitIds.every((id) => id.includes('gongan-collapse')) ||
|
|
fortyThirdCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected forty-third camp to expose Gongan collapse dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyThirdCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-gongan-collapse-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postGonganProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postGonganProgressState?.activeTab !== 'progress' ||
|
|
postGonganProgressState.campaignProgress?.completedKnown !== 43 ||
|
|
postGonganProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' ||
|
|
postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전'
|
|
) {
|
|
throw new Error(`Expected post-Gongan progress tab to complete the forty-third battle and point to Maicheng isolation: ${JSON.stringify(postGonganProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-gongan-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const maichengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!maichengSortieState?.sortieVisible ||
|
|
maichengSortieState.campaign?.step !== 'forty-third-camp' ||
|
|
maichengSortieState.sortiePlan?.objectiveLine !== '맥성 고립전 · 맥성 포위망 돌파' ||
|
|
maichengSortieState.sortiePlan?.maxCount !== 7 ||
|
|
maichengSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!maichengSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
!maichengSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!maichengSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended)
|
|
) {
|
|
throw new Error(`Expected Maicheng isolation sortie prep to lock Guan Yu and expose a seven-officer breakout choice: ${JSON.stringify(maichengSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(maichengSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const maichengPriorityUnits = ['wang-ping', 'zhao-yun', 'huang-zhong', 'ma-dai', 'ma-liang'];
|
|
for (const unitId of maichengPriorityUnits) {
|
|
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.required && !maichengPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const maichengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!maichengPriorityUnits.every((unitId) =>
|
|
maichengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!maichengSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!maichengSortieReadyState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.selected && unit.required) ||
|
|
maichengSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
maichengSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Maicheng isolation sortie to deploy Liu Bei, Guan Yu, Wang Ping, Zhao Yun, Huang Zhong, Ma Dai, and Ma Liang: ${JSON.stringify(maichengSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-maicheng-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-maicheng-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 55; i += 1) {
|
|
const enteredFortyFourthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-fourth-battle-maicheng-isolation';
|
|
});
|
|
if (enteredFortyFourthBattle) {
|
|
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 === 'forty-fourth-battle-maicheng-isolation' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-fourth-battle.png', fullPage: true });
|
|
|
|
const fortyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortyFourthEnemies = fortyFourthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortyFourthAllies = fortyFourthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortyFourthEnemyBehaviors = new Set(fortyFourthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortyFourthBattleState.camera?.mapWidth !== 88 ||
|
|
fortyFourthBattleState.camera?.mapHeight !== 70 ||
|
|
fortyFourthBattleState.victoryConditionLabel !== '맥성 포위망 돌파' ||
|
|
fortyFourthEnemies.length < 45 ||
|
|
!fortyFourthEnemyBehaviors.has('aggressive') ||
|
|
!fortyFourthEnemyBehaviors.has('guard') ||
|
|
!fortyFourthEnemyBehaviors.has('hold') ||
|
|
!fortyFourthEnemies.some((unit) => unit.id === 'maicheng-leader-lu-meng') ||
|
|
!fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-lu-xun') ||
|
|
!fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-pan-zhang') ||
|
|
!fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-ma-zhong') ||
|
|
!fortyFourthEnemies.some((unit) => unit.id === 'maicheng-officer-xu-huang') ||
|
|
!fortyFourthAllies.some((unit) => unit.id === 'guan-yu') ||
|
|
!fortyFourthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fortyFourthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fortyFourthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!fortyFourthAllies.some((unit) => unit.id === 'ma-dai')
|
|
) {
|
|
throw new Error(`Expected forty-fourth battle to use Maicheng isolation map, breakout objective, selected support officers, and mixed Wu/Wei AI: ${JSON.stringify(fortyFourthBattleState)}`);
|
|
}
|
|
|
|
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 fortyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortyFourthCampState?.campBattleId !== 'forty-fourth-battle-maicheng-isolation' ||
|
|
fortyFourthCampState.campTitle !== '맥성 고립전 후 군영' ||
|
|
fortyFourthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortyFourthCampState.availableDialogueIds.every((id) => id.includes('maicheng')) ||
|
|
fortyFourthCampState.availableVisitIds?.length !== 2 ||
|
|
!fortyFourthCampState.availableVisitIds.every((id) => id.includes('maicheng')) ||
|
|
fortyFourthCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected forty-fourth camp to expose Maicheng dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyFourthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-maicheng-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postMaichengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postMaichengProgressState?.activeTab !== 'progress' ||
|
|
postMaichengProgressState.campaignProgress?.completedKnown !== 44 ||
|
|
postMaichengProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' ||
|
|
postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로'
|
|
) {
|
|
throw new Error(`Expected post-Maicheng progress tab to complete the forty-fourth battle and point to Yiling vanguard: ${JSON.stringify(postMaichengProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-maicheng-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const yilingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!yilingSortieState?.sortieVisible ||
|
|
yilingSortieState.campaign?.step !== 'forty-fourth-camp' ||
|
|
yilingSortieState.sortiePlan?.objectiveLine !== '이릉 진격로 · 육손 전초선 격파' ||
|
|
yilingSortieState.sortiePlan?.maxCount !== 7 ||
|
|
yilingSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!yilingSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!yilingSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) ||
|
|
!yilingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recommended)
|
|
) {
|
|
throw new Error(`Expected Yiling vanguard sortie prep to lock Liu Bei and expose a seven-officer revenge-route choice: ${JSON.stringify(yilingSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(yilingSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const yilingPriorityUnits = ['zhang-fei', 'zhao-yun', 'ma-chao', 'zhuge-liang', 'huang-quan', 'ma-liang'];
|
|
for (const unitId of yilingPriorityUnits) {
|
|
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.required && !yilingPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const yilingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!yilingPriorityUnits.every((unitId) =>
|
|
yilingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!yilingSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
yilingSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
yilingSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Yiling vanguard sortie to deploy Liu Bei, Zhang Fei, Zhao Yun, Ma Chao, Zhuge Liang, Huang Quan, and Ma Liang: ${JSON.stringify(yilingSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-yiling-vanguard-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-yiling-vanguard-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 55; i += 1) {
|
|
const enteredFortyFifthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-fifth-battle-yiling-vanguard';
|
|
});
|
|
if (enteredFortyFifthBattle) {
|
|
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 === 'forty-fifth-battle-yiling-vanguard' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-fifth-battle.png', fullPage: true });
|
|
|
|
const fortyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortyFifthEnemies = fortyFifthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortyFifthAllies = fortyFifthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortyFifthEnemyBehaviors = new Set(fortyFifthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortyFifthBattleState.camera?.mapWidth !== 90 ||
|
|
fortyFifthBattleState.camera?.mapHeight !== 72 ||
|
|
fortyFifthBattleState.victoryConditionLabel !== '육손 전초선 격파' ||
|
|
fortyFifthEnemies.length < 38 ||
|
|
!fortyFifthEnemyBehaviors.has('aggressive') ||
|
|
!fortyFifthEnemyBehaviors.has('guard') ||
|
|
!fortyFifthEnemyBehaviors.has('hold') ||
|
|
!fortyFifthEnemies.some((unit) => unit.id === 'yiling-leader-lu-xun') ||
|
|
!fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-zhu-ran') ||
|
|
!fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-pan-zhang') ||
|
|
!fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-ma-zhong') ||
|
|
!fortyFifthEnemies.some((unit) => unit.id === 'yiling-officer-gan-ning-remnant') ||
|
|
!fortyFifthAllies.some((unit) => unit.id === 'zhang-fei') ||
|
|
!fortyFifthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fortyFifthAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!fortyFifthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fortyFifthAllies.some((unit) => unit.id === 'huang-quan')
|
|
) {
|
|
throw new Error(`Expected forty-fifth battle to use Yiling vanguard map, Lu Xun objective, selected revenge-route officers, and mixed Wu AI: ${JSON.stringify(fortyFifthBattleState)}`);
|
|
}
|
|
|
|
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 fortyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortyFifthCampState?.campBattleId !== 'forty-fifth-battle-yiling-vanguard' ||
|
|
fortyFifthCampState.campTitle !== '이릉 진격로 후 군영' ||
|
|
fortyFifthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortyFifthCampState.availableDialogueIds.every((id) => id.includes('yiling')) ||
|
|
fortyFifthCampState.availableVisitIds?.length !== 2 ||
|
|
!fortyFifthCampState.availableVisitIds.every((id) => id.includes('yiling')) ||
|
|
fortyFifthCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected forty-fifth camp to expose Yiling dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortyFifthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-yiling-vanguard-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postYilingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postYilingProgressState?.activeTab !== 'progress' ||
|
|
postYilingProgressState.campaignProgress?.completedKnown !== 45 ||
|
|
postYilingProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' ||
|
|
postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전'
|
|
) {
|
|
throw new Error(`Expected post-Yiling progress tab to complete the forty-fifth battle and point to the Yiling fire attack: ${JSON.stringify(postYilingProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-yiling-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const yilingFireSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!yilingFireSortieState?.sortieVisible ||
|
|
yilingFireSortieState.campaign?.step !== 'forty-fifth-camp' ||
|
|
yilingFireSortieState.sortiePlan?.objectiveLine !== '이릉 화공전 · 오군 화공망 돌파' ||
|
|
yilingFireSortieState.sortiePlan?.maxCount !== 7 ||
|
|
yilingFireSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!yilingFireSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
!yilingFireSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!yilingFireSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended)
|
|
) {
|
|
throw new Error(`Expected Yiling fire sortie prep to lock Liu Bei and expose a seven-officer firebreak choice: ${JSON.stringify(yilingFireSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(yilingFireSortieState, [
|
|
'liu-bei',
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const yilingFirePriorityUnits = ['huang-quan', 'zhuge-liang', 'ma-liang', 'zhao-yun', 'wang-ping', 'ma-chao'];
|
|
for (const unitId of yilingFirePriorityUnits) {
|
|
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.required && !yilingFirePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const yilingFireSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!yilingFirePriorityUnits.every((unitId) =>
|
|
yilingFireSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!yilingFireSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei' && unit.selected && unit.required) ||
|
|
yilingFireSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
yilingFireSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Yiling fire sortie to deploy Liu Bei, Huang Quan, Zhuge Liang, Ma Liang, Zhao Yun, Wang Ping, and Ma Chao: ${JSON.stringify(yilingFireSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-yiling-fire-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-yiling-fire-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 55; i += 1) {
|
|
const enteredFortySixthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-sixth-battle-yiling-fire';
|
|
});
|
|
if (enteredFortySixthBattle) {
|
|
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 === 'forty-sixth-battle-yiling-fire' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-sixth-battle.png', fullPage: true });
|
|
|
|
const fortySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortySixthEnemies = fortySixthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortySixthAllies = fortySixthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortySixthEnemyBehaviors = new Set(fortySixthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortySixthBattleState.camera?.mapWidth !== 92 ||
|
|
fortySixthBattleState.camera?.mapHeight !== 74 ||
|
|
fortySixthBattleState.victoryConditionLabel !== '오군 화공망 돌파' ||
|
|
fortySixthEnemies.length < 38 ||
|
|
!fortySixthEnemyBehaviors.has('aggressive') ||
|
|
!fortySixthEnemyBehaviors.has('guard') ||
|
|
!fortySixthEnemyBehaviors.has('hold') ||
|
|
!fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-leader-lu-xun') ||
|
|
!fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-zhu-ran') ||
|
|
!fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-han-dang') ||
|
|
!fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-pan-zhang') ||
|
|
!fortySixthEnemies.some((unit) => unit.id === 'yiling-fire-officer-ma-zhong') ||
|
|
!fortySixthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortySixthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fortySixthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortySixthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fortySixthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fortySixthAllies.some((unit) => unit.id === 'ma-chao')
|
|
) {
|
|
throw new Error(`Expected forty-sixth battle to use Yiling fire map, Lu Xun firebreak objective, selected retreat-route officers, and mixed Wu AI: ${JSON.stringify(fortySixthBattleState)}`);
|
|
}
|
|
|
|
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 fortySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortySixthCampState?.campBattleId !== 'forty-sixth-battle-yiling-fire' ||
|
|
fortySixthCampState.campTitle !== '이릉 화공전 후 군영' ||
|
|
fortySixthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortySixthCampState.availableDialogueIds.every((id) => id.includes('yiling-fire')) ||
|
|
fortySixthCampState.availableVisitIds?.length !== 2 ||
|
|
!fortySixthCampState.availableVisitIds.every((id) => id.includes('yiling-fire')) ||
|
|
fortySixthCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected forty-sixth camp to expose Yiling fire dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(fortySixthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-yiling-fire-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postYilingFireProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postYilingFireProgressState?.activeTab !== 'progress' ||
|
|
postYilingFireProgressState.campaignProgress?.completedKnown !== 46 ||
|
|
postYilingFireProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
|
|
postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁'
|
|
) {
|
|
throw new Error(`Expected post-Yiling-fire progress tab to complete the forty-sixth battle and point to Baidi entrustment: ${JSON.stringify(postYilingFireProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-yiling-fire-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const baidiEntrustmentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!baidiEntrustmentSortieState?.sortieVisible ||
|
|
baidiEntrustmentSortieState.campaign?.step !== 'forty-sixth-camp' ||
|
|
baidiEntrustmentSortieState.campTitle !== '이릉 화공전 후 군영'
|
|
) {
|
|
throw new Error(`Expected Baidi entrustment story to be available from the Yiling fire camp: ${JSON.stringify(baidiEntrustmentSortieState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-baidi-entrustment-prep.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-baidi-entrustment-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 30; i += 1) {
|
|
const returnedToBaidiCamp = await page.evaluate(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
const camp = window.__HEROS_DEBUG__?.camp?.();
|
|
return activeScenes.includes('CampScene') && camp?.campaign?.step === 'baidi-entrustment-camp';
|
|
});
|
|
if (returnedToBaidiCamp) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
const camp = window.__HEROS_DEBUG__?.camp?.();
|
|
return activeScenes.includes('CampScene') && camp?.campaign?.step === 'baidi-entrustment-camp';
|
|
});
|
|
|
|
const baidiCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
baidiCampState?.campaign?.step !== 'baidi-entrustment-camp' ||
|
|
baidiCampState.campTitle !== '백제성 유탁 후 군영' ||
|
|
baidiCampState.campBattleId !== 'forty-sixth-battle-yiling-fire' ||
|
|
baidiCampState.availableDialogueIds?.length !== 3 ||
|
|
!baidiCampState.availableDialogueIds.every((id) => id.includes('baidi')) ||
|
|
baidiCampState.availableVisitIds?.length !== 2 ||
|
|
!baidiCampState.availableVisitIds.every((id) => id.includes('baidi')) ||
|
|
baidiCampState.rosterCollection?.total < 22
|
|
) {
|
|
throw new Error(`Expected Baidi entrustment camp to expose Baidi-only dialogue/visit sets and preserve the full officer roster: ${JSON.stringify(baidiCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-baidi-entrustment-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postBaidiProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postBaidiProgressState?.activeTab !== 'progress' ||
|
|
postBaidiProgressState.campaignProgress?.completedKnown !== 46 ||
|
|
postBaidiProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' ||
|
|
postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로'
|
|
) {
|
|
throw new Error(`Expected Baidi entrustment progress tab to keep the forty-sixth battle complete and point to Nanzhong stabilization: ${JSON.stringify(postBaidiProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-baidi-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const nanzhongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!nanzhongSortieState?.sortieVisible ||
|
|
nanzhongSortieState.campaign?.step !== 'baidi-entrustment-camp' ||
|
|
nanzhongSortieState.campTitle !== '백제성 유탁 후 군영' ||
|
|
nanzhongSortieState.sortiePlan?.objectiveLine !== '남중 안정로 · 남중 반란 지휘선 진정' ||
|
|
nanzhongSortieState.sortiePlan?.maxCount !== 7 ||
|
|
nanzhongSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Nanzhong sortie prep to lock Zhuge Liang, recommend southern-route officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(nanzhongSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(nanzhongSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const nanzhongPriorityUnits = ['zhao-yun', 'wang-ping', 'ma-liang', 'huang-quan', 'huang-zhong', 'ma-chao'];
|
|
for (const unitId of nanzhongPriorityUnits) {
|
|
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.required && !nanzhongPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const nanzhongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!nanzhongPriorityUnits.every((unitId) =>
|
|
nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
nanzhongSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
nanzhongSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Nanzhong sortie to deploy Zhuge Liang, Zhao Yun, Wang Ping, Ma Liang, Huang Quan, Huang Zhong, and Ma Chao: ${JSON.stringify(nanzhongSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-nanzhong-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-nanzhong-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 55; i += 1) {
|
|
const enteredFortySeventhBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-seventh-battle-nanzhong-stabilization';
|
|
});
|
|
if (enteredFortySeventhBattle) {
|
|
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 === 'forty-seventh-battle-nanzhong-stabilization' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-seventh-battle.png', fullPage: true });
|
|
|
|
const fortySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortySeventhEnemies = fortySeventhBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortySeventhAllies = fortySeventhBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortySeventhEnemyBehaviors = new Set(fortySeventhEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortySeventhBattleState.camera?.mapWidth !== 94 ||
|
|
fortySeventhBattleState.camera?.mapHeight !== 76 ||
|
|
fortySeventhBattleState.victoryConditionLabel !== '남중 반란 지휘선 진정' ||
|
|
fortySeventhEnemies.length < 34 ||
|
|
!fortySeventhEnemyBehaviors.has('aggressive') ||
|
|
!fortySeventhEnemyBehaviors.has('guard') ||
|
|
!fortySeventhEnemyBehaviors.has('hold') ||
|
|
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-leader-yong-kai') ||
|
|
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-gao-ding') ||
|
|
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-zhu-bao') ||
|
|
!fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-meng-huo-vanguard') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!fortySeventhAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
fortySeventhAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected forty-seventh battle to use Nanzhong map, selected regency-route officers, and mixed southern rebel AI: ${JSON.stringify(fortySeventhBattleState)}`);
|
|
}
|
|
|
|
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 fortySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortySeventhCampState?.campBattleId !== 'forty-seventh-battle-nanzhong-stabilization' ||
|
|
fortySeventhCampState.campTitle !== '남중 안정로 후 군영' ||
|
|
fortySeventhCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortySeventhCampState.availableDialogueIds.every((id) => id.includes('nanzhong')) ||
|
|
fortySeventhCampState.availableVisitIds?.length !== 2 ||
|
|
!fortySeventhCampState.availableVisitIds.every((id) => id.includes('nanzhong')) ||
|
|
fortySeventhCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected forty-seventh camp to expose Nanzhong dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortySeventhCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-nanzhong-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postNanzhongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postNanzhongProgressState?.activeTab !== 'progress' ||
|
|
postNanzhongProgressState.campaignProgress?.completedKnown !== 47 ||
|
|
postNanzhongProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' ||
|
|
postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '맹획 본대전'
|
|
) {
|
|
throw new Error(`Expected post-Nanzhong progress tab to complete the forty-seventh battle and point to Meng Huo's main force: ${JSON.stringify(postNanzhongProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-nanzhong-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const mengHuoSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!mengHuoSortieState?.sortieVisible ||
|
|
mengHuoSortieState.campaign?.step !== 'forty-seventh-camp' ||
|
|
mengHuoSortieState.campTitle !== '남중 안정로 후 군영' ||
|
|
mengHuoSortieState.sortiePlan?.objectiveLine !== '맹획 본대전 · 맹획 본대 제압과 회유' ||
|
|
mengHuoSortieState.sortiePlan?.maxCount !== 7 ||
|
|
mengHuoSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
mengHuoSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Meng Huo sortie prep to lock Zhuge Liang, recommend pacification officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(mengHuoSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(mengHuoSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const mengHuoPriorityUnits = ['ma-liang', 'wang-ping', 'huang-quan', 'zhao-yun', 'ma-chao', 'huang-zhong'];
|
|
for (const unitId of mengHuoPriorityUnits) {
|
|
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.required && !mengHuoPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const mengHuoSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!mengHuoPriorityUnits.every((unitId) =>
|
|
mengHuoSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!mengHuoSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
mengHuoSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
mengHuoSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
mengHuoSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Wang Ping, Huang Quan, Zhao Yun, Ma Chao, and Huang Zhong: ${JSON.stringify(mengHuoSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-meng-huo-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-meng-huo-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 60; i += 1) {
|
|
const enteredFortyEighthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-eighth-battle-meng-huo-main-force';
|
|
});
|
|
if (enteredFortyEighthBattle) {
|
|
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 === 'forty-eighth-battle-meng-huo-main-force' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-eighth-battle.png', fullPage: true });
|
|
|
|
const fortyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortyEighthEnemies = fortyEighthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortyEighthAllies = fortyEighthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortyEighthEnemyBehaviors = new Set(fortyEighthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortyEighthBattleState.camera?.mapWidth !== 96 ||
|
|
fortyEighthBattleState.camera?.mapHeight !== 78 ||
|
|
fortyEighthBattleState.victoryConditionLabel !== '맹획 본대 제압과 회유' ||
|
|
!fortyEighthBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo' && objective.label === '맹획 생포와 회유') ||
|
|
fortyEighthEnemies.length < 33 ||
|
|
!fortyEighthEnemyBehaviors.has('aggressive') ||
|
|
!fortyEighthEnemyBehaviors.has('guard') ||
|
|
!fortyEighthEnemyBehaviors.has('hold') ||
|
|
!fortyEighthEnemies.some((unit) => unit.id === 'menghuo-leader') ||
|
|
!fortyEighthEnemies.some((unit) => unit.id === 'menghuo-officer-ahuinan') ||
|
|
!fortyEighthEnemies.some((unit) => unit.id === 'menghuo-officer-dongtuna') ||
|
|
!fortyEighthEnemies.some((unit) => unit.id === 'menghuo-officer-mangyachang') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!fortyEighthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
fortyEighthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected forty-eighth battle to use Meng Huo map, pacification objective, selected officers, and mixed Nanman AI: ${JSON.stringify(fortyEighthBattleState)}`);
|
|
}
|
|
|
|
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 fortyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortyEighthCampState?.campBattleId !== 'forty-eighth-battle-meng-huo-main-force' ||
|
|
fortyEighthCampState.campTitle !== '맹획 본대전 후 군영' ||
|
|
fortyEighthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortyEighthCampState.availableDialogueIds.every((id) => id.includes('menghuo')) ||
|
|
fortyEighthCampState.availableVisitIds?.length !== 2 ||
|
|
!fortyEighthCampState.availableVisitIds.every((id) => id.includes('menghuo')) ||
|
|
fortyEighthCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected forty-eighth camp to expose Meng Huo pacification dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortyEighthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-meng-huo-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postMengHuoProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postMengHuoProgressState?.activeTab !== 'progress' ||
|
|
postMengHuoProgressState.campaignProgress?.completedKnown !== 48 ||
|
|
postMengHuoProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postMengHuoProgressState.campaignProgress?.latestBattleTitle !== '맹획 본대전' ||
|
|
postMengHuoProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 2차전'
|
|
) {
|
|
throw new Error(`Expected post-Meng-Huo progress tab to complete the forty-eighth battle and point to the second capture arc: ${JSON.stringify(postMengHuoProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-meng-huo-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const secondCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!secondCaptureSortieState?.sortieVisible ||
|
|
secondCaptureSortieState.campaign?.step !== 'forty-eighth-camp' ||
|
|
secondCaptureSortieState.campTitle !== '맹획 본대전 후 군영' ||
|
|
secondCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 2차전 · 맹획 재생포와 마을 회유' ||
|
|
secondCaptureSortieState.sortiePlan?.maxCount !== 7 ||
|
|
secondCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
secondCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected second Meng Huo sortie prep to lock Zhuge Liang, recommend capture-and-release officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(secondCaptureSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(secondCaptureSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const secondCapturePriorityUnits = ['huang-quan', 'wang-ping', 'ma-liang', 'zhao-yun', 'huang-zhong', 'ma-chao'];
|
|
for (const unitId of secondCapturePriorityUnits) {
|
|
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.required && !secondCapturePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const secondCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!secondCapturePriorityUnits.every((unitId) =>
|
|
secondCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!secondCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
secondCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
secondCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
secondCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected second Meng Huo sortie to deploy Zhuge Liang, Huang Quan, Wang Ping, Ma Liang, Zhao Yun, Huang Zhong, and Ma Chao: ${JSON.stringify(secondCaptureSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-second-capture-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-second-capture-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 60; i += 1) {
|
|
const enteredFortyNinthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'forty-ninth-battle-meng-huo-second-capture';
|
|
});
|
|
if (enteredFortyNinthBattle) {
|
|
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 === 'forty-ninth-battle-meng-huo-second-capture' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-forty-ninth-battle.png', fullPage: true });
|
|
|
|
const fortyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fortyNinthEnemies = fortyNinthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fortyNinthAllies = fortyNinthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fortyNinthEnemyBehaviors = new Set(fortyNinthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fortyNinthBattleState.camera?.mapWidth !== 98 ||
|
|
fortyNinthBattleState.camera?.mapHeight !== 80 ||
|
|
fortyNinthBattleState.victoryConditionLabel !== '맹획 재생포와 마을 회유' ||
|
|
!fortyNinthBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-second' && objective.label === '맹획 두 번째 생포') ||
|
|
fortyNinthEnemies.length < 33 ||
|
|
!fortyNinthEnemyBehaviors.has('aggressive') ||
|
|
!fortyNinthEnemyBehaviors.has('guard') ||
|
|
!fortyNinthEnemyBehaviors.has('hold') ||
|
|
!fortyNinthEnemies.some((unit) => unit.id === 'menghuo-second-leader') ||
|
|
!fortyNinthEnemies.some((unit) => unit.id === 'menghuo-return-officer-meng-you') ||
|
|
!fortyNinthEnemies.some((unit) => unit.id === 'menghuo-return-officer-duosi') ||
|
|
!fortyNinthEnemies.some((unit) => unit.id === 'menghuo-return-officer-mulu') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!fortyNinthAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
fortyNinthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected forty-ninth battle to use second Meng Huo map, pacification objective, selected officers, and mixed Nanman AI: ${JSON.stringify(fortyNinthBattleState)}`);
|
|
}
|
|
|
|
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 fortyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fortyNinthCampState?.campBattleId !== 'forty-ninth-battle-meng-huo-second-capture' ||
|
|
fortyNinthCampState.campTitle !== '칠종칠금 2차전 후 군영' ||
|
|
fortyNinthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fortyNinthCampState.availableDialogueIds.every((id) => id.includes('second-capture')) ||
|
|
fortyNinthCampState.availableVisitIds?.length !== 2 ||
|
|
!fortyNinthCampState.availableVisitIds.every((id) => id.includes('second-capture')) ||
|
|
fortyNinthCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected forty-ninth camp to expose second capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortyNinthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-second-capture-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSecondCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSecondCaptureProgressState?.activeTab !== 'progress' ||
|
|
postSecondCaptureProgressState.campaignProgress?.completedKnown !== 49 ||
|
|
postSecondCaptureProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSecondCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 2차전' ||
|
|
postSecondCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 3차전'
|
|
) {
|
|
throw new Error(`Expected post-second-capture progress tab to complete the forty-ninth battle and point to the third capture arc: ${JSON.stringify(postSecondCaptureProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-second-capture-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const thirdCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!thirdCaptureSortieState?.sortieVisible ||
|
|
thirdCaptureSortieState.campaign?.step !== 'forty-ninth-camp' ||
|
|
thirdCaptureSortieState.campTitle !== '칠종칠금 2차전 후 군영' ||
|
|
thirdCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 3차전 · 맹획 세 번째 생포와 호족 회유' ||
|
|
thirdCaptureSortieState.sortiePlan?.maxCount !== 7 ||
|
|
thirdCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!thirdCaptureSortieState.sortiePlan?.recruitedLine?.includes('합류 무장') ||
|
|
thirdCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected third Meng Huo sortie prep to expose the expanded recruited roster, Zhuge Liang lock, and clan-persuasion recommendations: ${JSON.stringify(thirdCaptureSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(thirdCaptureSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const thirdCapturePriorityUnits = ['ma-liang', 'huang-quan', 'wang-ping', 'zhao-yun', 'ma-chao', 'huang-zhong'];
|
|
for (const unitId of thirdCapturePriorityUnits) {
|
|
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.required && !thirdCapturePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const thirdCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!thirdCapturePriorityUnits.every((unitId) =>
|
|
thirdCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!thirdCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
thirdCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
thirdCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
thirdCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected third Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Huang Quan, Wang Ping, Zhao Yun, Ma Chao, and Huang Zhong: ${JSON.stringify(thirdCaptureSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-third-capture-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-third-capture-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 60; i += 1) {
|
|
const enteredFiftiethBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fiftieth-battle-meng-huo-third-capture';
|
|
});
|
|
if (enteredFiftiethBattle) {
|
|
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 === 'fiftieth-battle-meng-huo-third-capture' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fiftieth-battle.png', fullPage: true });
|
|
|
|
const fiftiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftiethEnemies = fiftiethBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftiethAllies = fiftiethBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftiethEnemyBehaviors = new Set(fiftiethEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftiethBattleState.camera?.mapWidth !== 100 ||
|
|
fiftiethBattleState.camera?.mapHeight !== 82 ||
|
|
fiftiethBattleState.victoryConditionLabel !== '맹획 세 번째 생포와 호족 회유' ||
|
|
!fiftiethBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-third' && objective.label === '맹획 세 번째 생포') ||
|
|
fiftiethEnemies.length < 34 ||
|
|
!fiftiethEnemyBehaviors.has('aggressive') ||
|
|
!fiftiethEnemyBehaviors.has('guard') ||
|
|
!fiftiethEnemyBehaviors.has('hold') ||
|
|
!fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-leader') ||
|
|
!fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-dailai') ||
|
|
!fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-zhurong-vanguard') ||
|
|
!fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-jinhuan') ||
|
|
!fiftiethEnemies.some((unit) => unit.id === 'menghuo-third-officer-ahuinan') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!fiftiethAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
fiftiethAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fiftieth battle to use the third Meng Huo map, pacification objective, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftiethBattleState)}`);
|
|
}
|
|
|
|
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 fiftiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftiethCampState?.campBattleId !== 'fiftieth-battle-meng-huo-third-capture' ||
|
|
fiftiethCampState.campTitle !== '칠종칠금 3차전 후 군영' ||
|
|
fiftiethCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftiethCampState.availableDialogueIds.every((id) => id.includes('third-capture')) ||
|
|
fiftiethCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftiethCampState.availableVisitIds.every((id) => id.includes('third-capture')) ||
|
|
fiftiethCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected fiftieth camp to expose third capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftiethCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-third-capture-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postThirdCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postThirdCaptureProgressState?.activeTab !== 'progress' ||
|
|
postThirdCaptureProgressState.campaignProgress?.completedKnown !== 50 ||
|
|
postThirdCaptureProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postThirdCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 3차전' ||
|
|
postThirdCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 4차전'
|
|
) {
|
|
throw new Error(`Expected post-third-capture progress tab to complete the fiftieth battle and point to the fourth capture arc: ${JSON.stringify(postThirdCaptureProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-third-capture-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const fourthCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fourthCaptureSortieState?.sortieVisible ||
|
|
fourthCaptureSortieState.campaign?.step !== 'fiftieth-camp' ||
|
|
fourthCaptureSortieState.campTitle !== '칠종칠금 3차전 후 군영' ||
|
|
fourthCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 4차전 · 맹획 네 번째 생포와 회유 퇴로 유지' ||
|
|
fourthCaptureSortieState.sortiePlan?.maxCount !== 7 ||
|
|
fourthCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!fourthCaptureSortieState.sortiePlan?.recruitedLine?.includes('합류 무장') ||
|
|
fourthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fourth Meng Huo sortie prep to expose clan-persuasion recommendations, Zhuge Liang lock, and the expanded recruited roster: ${JSON.stringify(fourthCaptureSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(fourthCaptureSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const fourthCapturePriorityUnits = ['ma-liang', 'huang-quan', 'wang-ping', 'zhao-yun', 'ma-chao', 'ma-dai'];
|
|
for (const unitId of fourthCapturePriorityUnits) {
|
|
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.required && !fourthCapturePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const fourthCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fourthCapturePriorityUnits.every((unitId) =>
|
|
fourthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!fourthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
fourthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
fourthCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
fourthCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected fourth Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Huang Quan, Wang Ping, Zhao Yun, Ma Chao, and Ma Dai: ${JSON.stringify(fourthCaptureSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fourth-capture-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fourth-capture-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 70; i += 1) {
|
|
const enteredFiftyFirstBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-first-battle-meng-huo-fourth-capture';
|
|
});
|
|
if (enteredFiftyFirstBattle) {
|
|
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 === 'fifty-first-battle-meng-huo-fourth-capture' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-first-battle.png', fullPage: true });
|
|
|
|
const fiftyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftyFirstEnemies = fiftyFirstBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftyFirstAllies = fiftyFirstBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftyFirstEnemyBehaviors = new Set(fiftyFirstEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftyFirstBattleState.camera?.mapWidth !== 102 ||
|
|
fiftyFirstBattleState.camera?.mapHeight !== 84 ||
|
|
fiftyFirstBattleState.victoryConditionLabel !== '맹획 네 번째 생포와 회유 퇴로 유지' ||
|
|
!fiftyFirstBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-fourth' && objective.label === '맹획 네 번째 생포') ||
|
|
!fiftyFirstBattleState.objectives?.some((objective) => objective.id === 'amnesty-route' && objective.label === '회유 퇴로 유지') ||
|
|
fiftyFirstEnemies.length < 39 ||
|
|
!fiftyFirstEnemyBehaviors.has('aggressive') ||
|
|
!fiftyFirstEnemyBehaviors.has('guard') ||
|
|
!fiftyFirstEnemyBehaviors.has('hold') ||
|
|
!fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-leader') ||
|
|
!fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-wutugu') ||
|
|
!fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-mangyachang') ||
|
|
!fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-dongtuna') ||
|
|
!fiftyFirstEnemies.some((unit) => unit.id === 'menghuo-fourth-officer-ahuinan') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'ma-chao') ||
|
|
!fiftyFirstAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
fiftyFirstAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-first battle to use the fourth Meng Huo map, amnesty-route objective, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftyFirstBattleState)}`);
|
|
}
|
|
|
|
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 fiftyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftyFirstCampState?.campBattleId !== 'fifty-first-battle-meng-huo-fourth-capture' ||
|
|
fiftyFirstCampState.campTitle !== '칠종칠금 4차전 후 군영' ||
|
|
fiftyFirstCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftyFirstCampState.availableDialogueIds.every((id) => id.includes('fourth-capture')) ||
|
|
fiftyFirstCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftyFirstCampState.availableVisitIds.every((id) => id.includes('fourth-capture')) ||
|
|
fiftyFirstCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected fifty-first camp to expose fourth capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftyFirstCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fourth-capture-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFourthCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFourthCaptureProgressState?.activeTab !== 'progress' ||
|
|
postFourthCaptureProgressState.campaignProgress?.completedKnown !== 51 ||
|
|
postFourthCaptureProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFourthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 4차전' ||
|
|
postFourthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 5차전'
|
|
) {
|
|
throw new Error(`Expected post-fourth-capture progress tab to complete the fifty-first battle and point to the fifth capture arc: ${JSON.stringify(postFourthCaptureProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fourth-capture-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const fifthCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fifthCaptureSortieState?.sortieVisible ||
|
|
fifthCaptureSortieState.campaign?.step !== 'fifty-first-camp' ||
|
|
fifthCaptureSortieState.campTitle !== '칠종칠금 4차전 후 군영' ||
|
|
fifthCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 5차전 · 맹획 다섯 번째 생포와 강경파 분리' ||
|
|
fifthCaptureSortieState.sortiePlan?.maxCount !== 7 ||
|
|
fifthCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!fifthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!fifthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!fifthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!fifthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recommended) ||
|
|
!fifthCaptureSortieState.sortiePlan?.recruitedLine?.includes('합류 무장') ||
|
|
fifthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifth Meng Huo sortie prep to expose hardliner-separation recommendations, Zhuge Liang lock, and the expanded recruited roster: ${JSON.stringify(fifthCaptureSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(fifthCaptureSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const fifthCapturePriorityUnits = ['huang-quan', 'ma-liang', 'wang-ping', 'zhao-yun', 'huang-zhong', 'ma-dai'];
|
|
for (const unitId of fifthCapturePriorityUnits) {
|
|
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.required && !fifthCapturePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const fifthCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!fifthCapturePriorityUnits.every((unitId) =>
|
|
fifthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!fifthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
fifthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
fifthCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
fifthCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected fifth Meng Huo sortie to deploy Zhuge Liang, Huang Quan, Ma Liang, Wang Ping, Zhao Yun, Huang Zhong, and Ma Dai: ${JSON.stringify(fifthCaptureSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifth-capture-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fifth-capture-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 70; i += 1) {
|
|
const enteredFiftySecondBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-second-battle-meng-huo-fifth-capture';
|
|
});
|
|
if (enteredFiftySecondBattle) {
|
|
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 === 'fifty-second-battle-meng-huo-fifth-capture' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-second-battle.png', fullPage: true });
|
|
|
|
const fiftySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftySecondEnemies = fiftySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftySecondAllies = fiftySecondBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftySecondEnemyBehaviors = new Set(fiftySecondEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftySecondBattleState.camera?.mapWidth !== 104 ||
|
|
fiftySecondBattleState.camera?.mapHeight !== 86 ||
|
|
fiftySecondBattleState.victoryConditionLabel !== '맹획 다섯 번째 생포와 강경파 분리' ||
|
|
!fiftySecondBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-fifth' && objective.label === '맹획 다섯 번째 생포') ||
|
|
!fiftySecondBattleState.objectives?.some((objective) => objective.id === 'hardliner-separation' && objective.label === '강경파 분리로 확보') ||
|
|
fiftySecondEnemies.length < 40 ||
|
|
!fiftySecondEnemyBehaviors.has('aggressive') ||
|
|
!fiftySecondEnemyBehaviors.has('guard') ||
|
|
!fiftySecondEnemyBehaviors.has('hold') ||
|
|
!fiftySecondEnemies.some((unit) => unit.id === 'menghuo-fifth-leader') ||
|
|
!fiftySecondEnemies.some((unit) => unit.id === 'menghuo-fifth-officer-zhurong') ||
|
|
!fiftySecondEnemies.some((unit) => unit.id === 'menghuo-fifth-officer-mulu') ||
|
|
!fiftySecondEnemies.some((unit) => unit.id === 'menghuo-fifth-officer-duosi') ||
|
|
!fiftySecondEnemies.some((unit) => unit.id === 'menghuo-fifth-officer-mengyou') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
!fiftySecondAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
fiftySecondAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-second battle to use the fifth Meng Huo map, hardliner-separation objective, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftySecondBattleState)}`);
|
|
}
|
|
|
|
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 fiftySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftySecondCampState?.campBattleId !== 'fifty-second-battle-meng-huo-fifth-capture' ||
|
|
fiftySecondCampState.campTitle !== '칠종칠금 5차전 후 군영' ||
|
|
fiftySecondCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftySecondCampState.availableDialogueIds.every((id) => id.includes('fifth-capture')) ||
|
|
fiftySecondCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftySecondCampState.availableVisitIds.every((id) => id.includes('fifth-capture')) ||
|
|
fiftySecondCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected fifty-second camp to expose fifth capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftySecondCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifth-capture-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFifthCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFifthCaptureProgressState?.activeTab !== 'progress' ||
|
|
postFifthCaptureProgressState.campaignProgress?.completedKnown !== 52 ||
|
|
postFifthCaptureProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFifthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 5차전' ||
|
|
postFifthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 6차전'
|
|
) {
|
|
throw new Error(`Expected post-fifth-capture progress tab to complete the fifty-second battle and point to the sixth capture arc: ${JSON.stringify(postFifthCaptureProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fifth-capture-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const sixthCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!sixthCaptureSortieState?.sortieVisible ||
|
|
sixthCaptureSortieState.campaign?.step !== 'fifty-second-camp' ||
|
|
sixthCaptureSortieState.campTitle !== '칠종칠금 5차전 후 군영' ||
|
|
sixthCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 6차전 · 맹획 여섯 번째 생포와 마을 신뢰 유지' ||
|
|
sixthCaptureSortieState.sortiePlan?.maxCount !== 7 ||
|
|
sixthCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!sixthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!sixthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!sixthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!sixthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
sixthCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixth Meng Huo sortie prep to expose village-trust recommendations, Zhuge Liang lock, and the expanded recruited roster: ${JSON.stringify(sixthCaptureSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(sixthCaptureSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const sixthCapturePriorityUnits = ['ma-liang', 'huang-quan', 'wang-ping', 'zhao-yun', 'wei-yan', 'ma-dai'];
|
|
for (const unitId of sixthCapturePriorityUnits) {
|
|
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.required && !sixthCapturePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const sixthCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!sixthCapturePriorityUnits.every((unitId) =>
|
|
sixthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!sixthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
sixthCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
sixthCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
sixthCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected sixth Meng Huo sortie to deploy Zhuge Liang, Ma Liang, Huang Quan, Wang Ping, Zhao Yun, Wei Yan, and Ma Dai: ${JSON.stringify(sixthCaptureSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixth-capture-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-sixth-capture-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 70; i += 1) {
|
|
const enteredFiftyThirdBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-third-battle-meng-huo-sixth-capture';
|
|
});
|
|
if (enteredFiftyThirdBattle) {
|
|
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 === 'fifty-third-battle-meng-huo-sixth-capture' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-third-battle.png', fullPage: true });
|
|
|
|
const fiftyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftyThirdEnemies = fiftyThirdBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftyThirdAllies = fiftyThirdBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftyThirdEnemyBehaviors = new Set(fiftyThirdEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftyThirdBattleState.camera?.mapWidth !== 106 ||
|
|
fiftyThirdBattleState.camera?.mapHeight !== 88 ||
|
|
fiftyThirdBattleState.victoryConditionLabel !== '맹획 여섯 번째 생포와 마을 신뢰 유지' ||
|
|
!fiftyThirdBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-sixth' && objective.label === '맹획 여섯 번째 생포') ||
|
|
!fiftyThirdBattleState.objectives?.some((objective) => objective.id === 'village-trust' && objective.label === '마을 신뢰 유지') ||
|
|
fiftyThirdEnemies.length < 41 ||
|
|
!fiftyThirdEnemyBehaviors.has('aggressive') ||
|
|
!fiftyThirdEnemyBehaviors.has('guard') ||
|
|
!fiftyThirdEnemyBehaviors.has('hold') ||
|
|
!fiftyThirdEnemies.some((unit) => unit.id === 'menghuo-sixth-leader') ||
|
|
!fiftyThirdEnemies.some((unit) => unit.id === 'menghuo-sixth-officer-dailai') ||
|
|
!fiftyThirdEnemies.some((unit) => unit.id === 'menghuo-sixth-officer-zhurong') ||
|
|
!fiftyThirdEnemies.some((unit) => unit.id === 'menghuo-sixth-officer-mulu') ||
|
|
!fiftyThirdEnemies.some((unit) => unit.id === 'menghuo-sixth-officer-duosi') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!fiftyThirdAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
fiftyThirdAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-third battle to use the sixth Meng Huo map, village-trust objective, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftyThirdBattleState)}`);
|
|
}
|
|
|
|
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 fiftyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftyThirdCampState?.campBattleId !== 'fifty-third-battle-meng-huo-sixth-capture' ||
|
|
fiftyThirdCampState.campTitle !== '칠종칠금 6차전 후 군영' ||
|
|
fiftyThirdCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftyThirdCampState.availableDialogueIds.every((id) => id.includes('sixth-capture')) ||
|
|
fiftyThirdCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftyThirdCampState.availableVisitIds.every((id) => id.includes('sixth-capture')) ||
|
|
fiftyThirdCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected fifty-third camp to expose sixth capture dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftyThirdCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixth-capture-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixthCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixthCaptureProgressState?.activeTab !== 'progress' ||
|
|
postSixthCaptureProgressState.campaignProgress?.completedKnown !== 53 ||
|
|
postSixthCaptureProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixthCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 6차전' ||
|
|
postSixthCaptureProgressState.campaignProgress?.nextBattleTitle !== '칠종칠금 7차전'
|
|
) {
|
|
throw new Error(`Expected post-sixth-capture progress tab to complete the fifty-third battle and point to the final capture arc: ${JSON.stringify(postSixthCaptureProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixth-capture-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const finalCaptureSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!finalCaptureSortieState?.sortieVisible ||
|
|
finalCaptureSortieState.campaign?.step !== 'fifty-third-camp' ||
|
|
finalCaptureSortieState.campTitle !== '칠종칠금 6차전 후 군영' ||
|
|
finalCaptureSortieState.sortiePlan?.objectiveLine !== '칠종칠금 7차전 · 맹획 마지막 생포와 남중 회의장 보호' ||
|
|
finalCaptureSortieState.sortiePlan?.maxCount !== 7 ||
|
|
finalCaptureSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!finalCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!finalCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!finalCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!finalCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
finalCaptureSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected final Meng Huo sortie prep to expose council-protection recommendations, Zhuge Liang lock, and the expanded recruited roster: ${JSON.stringify(finalCaptureSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(finalCaptureSortieState, [
|
|
'guan-yu',
|
|
'zhang-fei',
|
|
'jian-yong',
|
|
'mi-zhu',
|
|
'sun-qian',
|
|
'zhao-yun',
|
|
'zhuge-liang',
|
|
'ma-liang',
|
|
'yi-ji',
|
|
'gong-zhi',
|
|
'huang-zhong',
|
|
'wei-yan',
|
|
'pang-tong',
|
|
'fa-zheng',
|
|
'wu-yi',
|
|
'yan-yan',
|
|
'li-yan',
|
|
'huang-quan',
|
|
'ma-chao',
|
|
'ma-dai',
|
|
'wang-ping'
|
|
]);
|
|
|
|
const finalCapturePriorityUnits = ['huang-quan', 'ma-liang', 'wang-ping', 'zhao-yun', 'ma-dai', 'huang-zhong'];
|
|
for (const unitId of finalCapturePriorityUnits) {
|
|
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.required && !finalCapturePriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const finalCaptureSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!finalCapturePriorityUnits.every((unitId) =>
|
|
finalCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!finalCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
finalCaptureSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
finalCaptureSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
finalCaptureSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected final Meng Huo sortie to deploy Zhuge Liang, Huang Quan, Ma Liang, Wang Ping, Zhao Yun, Ma Dai, and Huang Zhong: ${JSON.stringify(finalCaptureSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-final-capture-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-final-capture-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 70; i += 1) {
|
|
const enteredFiftyFourthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fourth-battle-meng-huo-final-capture';
|
|
});
|
|
if (enteredFiftyFourthBattle) {
|
|
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 === 'fifty-fourth-battle-meng-huo-final-capture' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-fourth-battle.png', fullPage: true });
|
|
|
|
const fiftyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftyFourthEnemies = fiftyFourthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftyFourthAllies = fiftyFourthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftyFourthEnemyBehaviors = new Set(fiftyFourthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftyFourthBattleState.camera?.mapWidth !== 108 ||
|
|
fiftyFourthBattleState.camera?.mapHeight !== 90 ||
|
|
fiftyFourthBattleState.victoryConditionLabel !== '맹획 마지막 생포와 남중 회의장 보호' ||
|
|
!fiftyFourthBattleState.objectives?.some((objective) => objective.id === 'pacify-meng-huo-final' && objective.label === '맹획 일곱 번째 생포') ||
|
|
!fiftyFourthBattleState.objectives?.some((objective) => objective.id === 'village-witnesses' && objective.label === '촌장 증언 마을 보호') ||
|
|
fiftyFourthEnemies.length < 43 ||
|
|
!fiftyFourthEnemyBehaviors.has('aggressive') ||
|
|
!fiftyFourthEnemyBehaviors.has('guard') ||
|
|
!fiftyFourthEnemyBehaviors.has('hold') ||
|
|
!fiftyFourthEnemies.some((unit) => unit.id === 'menghuo-final-leader') ||
|
|
!fiftyFourthEnemies.some((unit) => unit.id === 'menghuo-final-officer-ahuinan') ||
|
|
!fiftyFourthEnemies.some((unit) => unit.id === 'menghuo-final-officer-zhurong') ||
|
|
!fiftyFourthEnemies.some((unit) => unit.id === 'menghuo-final-officer-mulu') ||
|
|
!fiftyFourthEnemies.some((unit) => unit.id === 'menghuo-final-officer-duosi') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!fiftyFourthAllies.some((unit) => unit.id === 'huang-zhong') ||
|
|
fiftyFourthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-fourth battle to use the final Meng Huo map, council/village objectives, selected recruited officers, and mixed clan AI: ${JSON.stringify(fiftyFourthBattleState)}`);
|
|
}
|
|
|
|
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 fiftyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftyFourthCampState?.campBattleId !== 'fifty-fourth-battle-meng-huo-final-capture' ||
|
|
fiftyFourthCampState.campTitle !== '남중 평정 후 군영' ||
|
|
fiftyFourthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftyFourthCampState.availableDialogueIds.every((id) => id.includes('final-capture')) ||
|
|
fiftyFourthCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftyFourthCampState.availableVisitIds.every((id) => id.includes('final-capture')) ||
|
|
fiftyFourthCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected final capture camp to expose Nanzhong pacification dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftyFourthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-final-capture-camp.png', fullPage: true });
|
|
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFinalCaptureProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFinalCaptureProgressState?.activeTab !== 'progress' ||
|
|
postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 ||
|
|
postFinalCaptureProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFinalCaptureProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postFinalCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' ||
|
|
!String(postFinalCaptureProgressState.campaignProgress?.nextBattleTitle ?? '').includes('북벌 준비')
|
|
) {
|
|
throw new Error(`Expected post-final-capture progress tab to complete the fifty-fourth battle and point toward northern campaign preparation: ${JSON.stringify(postFinalCaptureProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-final-capture-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const northernPrepSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!northernPrepSortieState?.sortieVisible ||
|
|
northernPrepSortieState.campaign?.step !== 'fifty-fourth-camp' ||
|
|
!String(northernPrepSortieState.campTitle ?? '').includes('남중 평정') ||
|
|
!String(northernPrepSortieState.sortiePlan?.objectiveLine ?? '').includes('북벌 준비') ||
|
|
northernPrepSortieState.sortiePlan?.maxCount !== 7 ||
|
|
northernPrepSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected northern campaign prep sortie panel to center Zhuge Liang, exclude Liu Bei, and expose a seven-officer planning roster: ${JSON.stringify(northernPrepSortieState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-northern-prep-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-northern-prep-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 18; i += 1) {
|
|
const returnedToCamp = await page.evaluate(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene');
|
|
});
|
|
if (returnedToCamp) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene');
|
|
});
|
|
|
|
const northernPrepCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
northernPrepCampState?.campaign?.step !== 'northern-campaign-prep-camp' ||
|
|
!String(northernPrepCampState.campTitle ?? '').includes('북벌 준비') ||
|
|
northernPrepCampState.campBattleId !== 'fifty-fourth-battle-meng-huo-final-capture' ||
|
|
northernPrepCampState.availableDialogueIds?.length !== 3 ||
|
|
!northernPrepCampState.availableDialogueIds.every((id) => id.startsWith('northern-prep-')) ||
|
|
northernPrepCampState.availableVisitIds?.length !== 2 ||
|
|
!northernPrepCampState.availableVisitIds.every((id) => id.startsWith('northern-prep-')) ||
|
|
northernPrepCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected northern campaign prep story to persist a dedicated camp step with preparation dialogue and visit sets: ${JSON.stringify(northernPrepCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-northern-prep-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postNorthernPrepProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postNorthernPrepProgressState?.activeTab !== 'progress' ||
|
|
postNorthernPrepProgressState.campaignProgress?.completedKnown !== 54 ||
|
|
postNorthernPrepProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postNorthernPrepProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postNorthernPrepProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' ||
|
|
postNorthernPrepProgressState.campaignProgress?.nextBattleTitle !== '제1차 북벌 출진로'
|
|
) {
|
|
throw new Error(`Expected northern campaign prep progress tab to point at the first northern expedition battle: ${JSON.stringify(postNorthernPrepProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-northern-prep-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const firstNorthernSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!firstNorthernSortieState?.sortieVisible ||
|
|
firstNorthernSortieState.campaign?.step !== 'northern-campaign-prep-camp' ||
|
|
!String(firstNorthernSortieState.campTitle ?? '').includes('북벌 준비') ||
|
|
!String(firstNorthernSortieState.sortiePlan?.objectiveLine ?? '').includes('제1차 북벌 출진로') ||
|
|
!String(firstNorthernSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 산길') ||
|
|
firstNorthernSortieState.sortiePlan?.maxCount !== 7 ||
|
|
firstNorthernSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
firstNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected first northern expedition sortie to require Zhuge Liang, exclude Liu Bei, and recommend the Qishan planning roster: ${JSON.stringify(firstNorthernSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(firstNorthernSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping'
|
|
]);
|
|
|
|
const firstNorthernPriorityUnits = ['zhao-yun', 'huang-quan', 'ma-liang', 'wang-ping', 'ma-dai', 'wei-yan'];
|
|
for (const unitId of firstNorthernPriorityUnits) {
|
|
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.required && !firstNorthernPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const firstNorthernSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!firstNorthernPriorityUnits.every((unitId) =>
|
|
firstNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!firstNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
firstNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
firstNorthernSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
firstNorthernSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected first northern expedition sortie to deploy Zhuge Liang, Zhao Yun, Huang Quan, Ma Liang, Wang Ping, Ma Dai, and Wei Yan: ${JSON.stringify(firstNorthernSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-fifth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fifty-fifth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredFiftyFifthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-fifth-battle-northern-qishan-road';
|
|
});
|
|
if (enteredFiftyFifthBattle) {
|
|
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 === 'fifty-fifth-battle-northern-qishan-road' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-fifth-battle.png', fullPage: true });
|
|
|
|
const fiftyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftyFifthEnemies = fiftyFifthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftyFifthAllies = fiftyFifthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftyFifthEnemyBehaviors = new Set(fiftyFifthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftyFifthBattleState.camera?.mapWidth !== 110 ||
|
|
fiftyFifthBattleState.camera?.mapHeight !== 92 ||
|
|
fiftyFifthBattleState.victoryConditionLabel !== '기산 산길 돌파와 한중 보급로 확보' ||
|
|
!fiftyFifthBattleState.objectives?.some((objective) => objective.id === 'repel-cao-zhen' && objective.label === '조진 전초대 격퇴') ||
|
|
!fiftyFifthBattleState.objectives?.some((objective) => objective.id === 'qishan-road' && objective.label === '기산 산길 확보') ||
|
|
!fiftyFifthBattleState.objectives?.some((objective) => objective.id === 'hanzhong-supply-camp' && objective.label === '한중 보급 거점 유지') ||
|
|
fiftyFifthEnemies.length < 42 ||
|
|
!fiftyFifthEnemyBehaviors.has('aggressive') ||
|
|
!fiftyFifthEnemyBehaviors.has('guard') ||
|
|
!fiftyFifthEnemyBehaviors.has('hold') ||
|
|
!fiftyFifthEnemies.some((unit) => unit.id === 'northern-first-leader-cao-zhen') ||
|
|
!fiftyFifthEnemies.some((unit) => unit.id === 'northern-first-officer-zhang-he') ||
|
|
!fiftyFifthEnemies.some((unit) => unit.id === 'northern-first-officer-guo-huai') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!fiftyFifthAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
fiftyFifthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-fifth battle to use the Qishan map, first-northern objectives, selected recruited officers, and mixed Wei AI: ${JSON.stringify(fiftyFifthBattleState)}`);
|
|
}
|
|
|
|
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 fiftyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftyFifthCampState?.campBattleId !== 'fifty-fifth-battle-northern-qishan-road' ||
|
|
fiftyFifthCampState.campaign?.step !== 'fifty-fifth-camp' ||
|
|
fiftyFifthCampState.campTitle !== '기산 출진로 후 군영' ||
|
|
fiftyFifthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftyFifthCampState.availableDialogueIds.every((id) => id.startsWith('northern-first-')) ||
|
|
fiftyFifthCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftyFifthCampState.availableVisitIds.every((id) => id.startsWith('northern-first-')) ||
|
|
fiftyFifthCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected first northern expedition camp to expose Qishan dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fiftyFifthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-fifth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFiftyFifthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFiftyFifthProgressState?.activeTab !== 'progress' ||
|
|
postFiftyFifthProgressState.campaignProgress?.completedKnown !== 55 ||
|
|
postFiftyFifthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFiftyFifthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postFiftyFifthProgressState.campaignProgress?.latestBattleTitle !== '제1차 북벌 출진로' ||
|
|
postFiftyFifthProgressState.campaignProgress?.nextBattleTitle !== '천수 진군로'
|
|
) {
|
|
throw new Error(`Expected post-fifty-fifth progress tab to complete the first northern expedition battle and point toward Tianshui: ${JSON.stringify(postFiftyFifthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fifty-fifth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const secondNorthernSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!secondNorthernSortieState?.sortieVisible ||
|
|
secondNorthernSortieState.campaign?.step !== 'fifty-fifth-camp' ||
|
|
secondNorthernSortieState.campTitle !== '기산 출진로 후 군영' ||
|
|
!String(secondNorthernSortieState.sortiePlan?.objectiveLine ?? '').includes('천수 진군로') ||
|
|
!String(secondNorthernSortieState.sortiePlan?.objectiveLine ?? '').includes('곽회') ||
|
|
secondNorthernSortieState.sortiePlan?.maxCount !== 7 ||
|
|
secondNorthernSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recommended) ||
|
|
!secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
secondNorthernSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected second northern expedition sortie to require Zhuge Liang, exclude Liu Bei, and recommend the Tianshui planning roster: ${JSON.stringify(secondNorthernSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(secondNorthernSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping'
|
|
]);
|
|
|
|
const secondNorthernPriorityUnits = ['wang-ping', 'zhao-yun', 'huang-quan', 'ma-liang', 'fa-zheng', 'ma-dai'];
|
|
for (const unitId of secondNorthernPriorityUnits) {
|
|
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.required && !secondNorthernPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const secondNorthernSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!secondNorthernPriorityUnits.every((unitId) =>
|
|
secondNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!secondNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
secondNorthernSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
secondNorthernSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
secondNorthernSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Tianshui sortie to deploy Zhuge Liang, Wang Ping, Zhao Yun, Huang Quan, Ma Liang, Fa Zheng, and Ma Dai: ${JSON.stringify(secondNorthernSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-sixth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fifty-sixth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredFiftySixthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-sixth-battle-tianshui-advance';
|
|
});
|
|
if (enteredFiftySixthBattle) {
|
|
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 === 'fifty-sixth-battle-tianshui-advance' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-sixth-battle.png', fullPage: true });
|
|
|
|
const fiftySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftySixthEnemies = fiftySixthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftySixthAllies = fiftySixthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftySixthEnemyBehaviors = new Set(fiftySixthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftySixthBattleState.camera?.mapWidth !== 112 ||
|
|
fiftySixthBattleState.camera?.mapHeight !== 94 ||
|
|
fiftySixthBattleState.victoryConditionLabel !== '천수 길목 돌파와 곽회 방어선 격퇴' ||
|
|
!fiftySixthBattleState.objectives?.some((objective) => objective.id === 'repel-guo-huai' && objective.label === '곽회 방어선 격퇴') ||
|
|
!fiftySixthBattleState.objectives?.some((objective) => objective.id === 'test-jiang-wei' && objective.label === '강유의 전술 확인') ||
|
|
!fiftySixthBattleState.objectives?.some((objective) => objective.id === 'jieting-forts' && objective.label === '가정 고개 요새 압박') ||
|
|
fiftySixthEnemies.length < 42 ||
|
|
!fiftySixthEnemyBehaviors.has('aggressive') ||
|
|
!fiftySixthEnemyBehaviors.has('guard') ||
|
|
!fiftySixthEnemyBehaviors.has('hold') ||
|
|
!fiftySixthEnemies.some((unit) => unit.id === 'northern-second-leader-guo-huai') ||
|
|
!fiftySixthEnemies.some((unit) => unit.id === 'northern-second-officer-jiang-wei') ||
|
|
!fiftySixthEnemies.some((unit) => unit.id === 'northern-second-officer-xiahou-mao') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!fiftySixthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
fiftySixthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-sixth battle to use the Tianshui map, Jiang Wei hook objectives, selected recruited officers, and mixed Wei AI: ${JSON.stringify(fiftySixthBattleState)}`);
|
|
}
|
|
|
|
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 fiftySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftySixthCampState?.campBattleId !== 'fifty-sixth-battle-tianshui-advance' ||
|
|
fiftySixthCampState.campaign?.step !== 'fifty-sixth-camp' ||
|
|
fiftySixthCampState.campTitle !== '천수 진군로 후 군영' ||
|
|
fiftySixthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftySixthCampState.availableDialogueIds.every((id) => id.startsWith('northern-second-')) ||
|
|
fiftySixthCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftySixthCampState.availableVisitIds.every((id) => id.startsWith('northern-second-')) ||
|
|
fiftySixthCampState.rosterCollection?.total < 21
|
|
) {
|
|
throw new Error(`Expected Tianshui advance camp to expose Jiang Wei/Jieting dialogue and visit sets while preserving the deployable officer roster: ${JSON.stringify(fiftySixthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-sixth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFiftySixthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFiftySixthProgressState?.activeTab !== 'progress' ||
|
|
postFiftySixthProgressState.campaignProgress?.completedKnown !== 56 ||
|
|
postFiftySixthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFiftySixthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postFiftySixthProgressState.campaignProgress?.latestBattleTitle !== '천수 진군로' ||
|
|
postFiftySixthProgressState.campaignProgress?.nextBattleTitle !== '가정 배치 위기'
|
|
) {
|
|
throw new Error(`Expected post-fifty-sixth progress tab to complete the Tianshui advance and point toward Jieting: ${JSON.stringify(postFiftySixthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fifty-sixth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const jietingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jietingSortieState?.sortieVisible ||
|
|
jietingSortieState.campaign?.step !== 'fifty-sixth-camp' ||
|
|
jietingSortieState.campTitle !== '천수 진군로 후 군영' ||
|
|
!String(jietingSortieState.sortiePlan?.objectiveLine ?? '').includes('가정 배치 위기') ||
|
|
!String(jietingSortieState.sortiePlan?.objectiveLine ?? '').includes('장합') ||
|
|
jietingSortieState.sortiePlan?.maxCount !== 7 ||
|
|
jietingSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recommended) ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!jietingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
jietingSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
jietingSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei')
|
|
) {
|
|
throw new Error(`Expected Jieting sortie to require Zhuge Liang, exclude Liu Bei, and make terrain readers central before Jiang Wei joins: ${JSON.stringify(jietingSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(jietingSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping'
|
|
]);
|
|
|
|
const jietingPriorityUnits = ['wang-ping', 'ma-liang', 'huang-quan', 'fa-zheng', 'zhao-yun', 'ma-dai'];
|
|
for (const unitId of jietingPriorityUnits) {
|
|
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.required && !jietingPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const jietingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!jietingPriorityUnits.every((unitId) =>
|
|
jietingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!jietingSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
jietingSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
jietingSortieReadyState.sortieRoster?.some((unit) => unit.id === 'jiang-wei') ||
|
|
jietingSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
jietingSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Jieting sortie to deploy Zhuge Liang, Wang Ping, Ma Liang, Huang Quan, Fa Zheng, Zhao Yun, and Ma Dai: ${JSON.stringify(jietingSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-seventh-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fifty-seventh-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredFiftySeventhBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-seventh-battle-jieting-crisis';
|
|
});
|
|
if (enteredFiftySeventhBattle) {
|
|
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 === 'fifty-seventh-battle-jieting-crisis' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-seventh-battle.png', fullPage: true });
|
|
|
|
const fiftySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftySeventhEnemies = fiftySeventhBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftySeventhAllies = fiftySeventhBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftySeventhEnemyBehaviors = new Set(fiftySeventhEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftySeventhBattleState.camera?.mapWidth !== 114 ||
|
|
fiftySeventhBattleState.camera?.mapHeight !== 96 ||
|
|
fiftySeventhBattleState.victoryConditionLabel !== '가정 고지와 물길 확보 및 장합 선봉 저지' ||
|
|
!fiftySeventhBattleState.objectives?.some((objective) => objective.id === 'repel-zhang-he' && objective.label === '장합 선봉 저지') ||
|
|
!fiftySeventhBattleState.objectives?.some((objective) => objective.id === 'contact-jiang-wei' && objective.label === '강유 접촉로 확보') ||
|
|
!fiftySeventhBattleState.objectives?.some((objective) => objective.id === 'wang-ping' && objective.label === '왕평 생존') ||
|
|
!fiftySeventhBattleState.objectives?.some((objective) => objective.id === 'jieting-waterline' && objective.label === '가정 물길 확보') ||
|
|
fiftySeventhEnemies.length < 42 ||
|
|
!fiftySeventhEnemyBehaviors.has('aggressive') ||
|
|
!fiftySeventhEnemyBehaviors.has('guard') ||
|
|
!fiftySeventhEnemyBehaviors.has('hold') ||
|
|
!fiftySeventhEnemies.some((unit) => unit.id === 'northern-third-leader-zhang-he') ||
|
|
!fiftySeventhEnemies.some((unit) => unit.id === 'northern-third-officer-jiang-wei') ||
|
|
!fiftySeventhEnemies.some((unit) => unit.id === 'northern-third-officer-masu-pressure') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftySeventhAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
fiftySeventhAllies.some((unit) => unit.id === 'liu-bei') ||
|
|
fiftySeventhAllies.some((unit) => unit.id === 'jiang-wei')
|
|
) {
|
|
throw new Error(`Expected fifty-seventh battle to use the Jieting map, Wang Ping waterline objectives, Jiang Wei contact hook, and mixed Wei AI: ${JSON.stringify(fiftySeventhBattleState)}`);
|
|
}
|
|
|
|
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 fiftySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftySeventhCampState?.campBattleId !== 'fifty-seventh-battle-jieting-crisis' ||
|
|
fiftySeventhCampState.campaign?.step !== 'fifty-seventh-camp' ||
|
|
fiftySeventhCampState.campTitle !== '가정 배치 위기 후 군영' ||
|
|
fiftySeventhCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftySeventhCampState.availableDialogueIds.every((id) => id.startsWith('northern-third-')) ||
|
|
fiftySeventhCampState.availableVisitIds?.length !== 2 ||
|
|
!fiftySeventhCampState.availableVisitIds.every((id) => id.startsWith('northern-third-')) ||
|
|
fiftySeventhCampState.rosterCollection?.total < 22 ||
|
|
!fiftySeventhCampState.campaign?.roster?.some((unit) => unit.id === 'jiang-wei')
|
|
) {
|
|
throw new Error(`Expected Jieting aftermath camp to add Jiang Wei and expose Jieting recovery events: ${JSON.stringify(fiftySeventhCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-seventh-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFiftySeventhProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFiftySeventhProgressState?.activeTab !== 'progress' ||
|
|
postFiftySeventhProgressState.campaignProgress?.completedKnown !== 57 ||
|
|
postFiftySeventhProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFiftySeventhProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postFiftySeventhProgressState.campaignProgress?.latestBattleTitle !== '가정 배치 위기' ||
|
|
postFiftySeventhProgressState.campaignProgress?.nextBattleTitle !== '기산 후퇴로'
|
|
) {
|
|
throw new Error(`Expected post-fifty-seventh progress tab to complete the Jieting crisis and point toward the Qishan retreat: ${JSON.stringify(postFiftySeventhProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fifty-seventh-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const qishanRetreatSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!qishanRetreatSortieState?.sortieVisible ||
|
|
qishanRetreatSortieState.campaign?.step !== 'fifty-seventh-camp' ||
|
|
qishanRetreatSortieState.campTitle !== '가정 배치 위기 후 군영' ||
|
|
!String(qishanRetreatSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 후퇴로') ||
|
|
!String(qishanRetreatSortieState.sortiePlan?.objectiveLine ?? '').includes('사마의') ||
|
|
qishanRetreatSortieState.sortiePlan?.maxCount !== 7 ||
|
|
qishanRetreatSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
qishanRetreatSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Qishan retreat sortie to require Zhuge Liang, exclude Liu Bei, and recommend Jiang Wei's first selectable sortie role: ${JSON.stringify(qishanRetreatSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(qishanRetreatSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const qishanPriorityUnits = ['jiang-wei', 'wang-ping', 'zhao-yun', 'ma-dai', 'huang-quan', 'ma-liang'];
|
|
for (const unitId of qishanPriorityUnits) {
|
|
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.required && !qishanPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const qishanRetreatSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!qishanPriorityUnits.every((unitId) =>
|
|
qishanRetreatSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!qishanRetreatSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
qishanRetreatSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
qishanRetreatSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
qishanRetreatSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Qishan retreat sortie to deploy Zhuge Liang, Jiang Wei, Wang Ping, Zhao Yun, Ma Dai, Huang Quan, and Ma Liang: ${JSON.stringify(qishanRetreatSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-eighth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fifty-eighth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredFiftyEighthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-eighth-battle-qishan-retreat';
|
|
});
|
|
if (enteredFiftyEighthBattle) {
|
|
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 === 'fifty-eighth-battle-qishan-retreat' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-eighth-battle.png', fullPage: true });
|
|
|
|
const fiftyEighthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftyEighthEnemies = fiftyEighthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftyEighthAllies = fiftyEighthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftyEighthEnemyBehaviors = new Set(fiftyEighthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftyEighthBattleState.camera?.mapWidth !== 116 ||
|
|
fiftyEighthBattleState.camera?.mapHeight !== 98 ||
|
|
fiftyEighthBattleState.victoryConditionLabel !== '기산 퇴로와 군량 수레 확보 및 사마의 본대 저지' ||
|
|
!fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'repel-sima-yi' && objective.label === '사마의 본대 저지') ||
|
|
!fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'halt-zhang-he' && objective.label === '장합 추격 차단') ||
|
|
!fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'qishan-cart-road' && objective.label === '기산 군량 수레 길 확보') ||
|
|
!fiftyEighthBattleState.objectives?.some((objective) => objective.id === 'qishan-waterline' && objective.label === '퇴각 물길 확보') ||
|
|
fiftyEighthEnemies.length < 42 ||
|
|
!fiftyEighthEnemyBehaviors.has('aggressive') ||
|
|
!fiftyEighthEnemyBehaviors.has('guard') ||
|
|
!fiftyEighthEnemyBehaviors.has('hold') ||
|
|
!fiftyEighthEnemies.some((unit) => unit.id === 'northern-fourth-leader-sima-yi') ||
|
|
!fiftyEighthEnemies.some((unit) => unit.id === 'northern-fourth-officer-zhang-he') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'jiang-wei') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftyEighthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
fiftyEighthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-eighth battle to use the Qishan retreat map, Sima Yi/Zhang He objectives, selected Jiang Wei sortie, and mixed Wei AI: ${JSON.stringify(fiftyEighthBattleState)}`);
|
|
}
|
|
|
|
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 fiftyEighthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftyEighthCampState?.campBattleId !== 'fifty-eighth-battle-qishan-retreat' ||
|
|
fiftyEighthCampState.campaign?.step !== 'fifty-eighth-camp' ||
|
|
fiftyEighthCampState.campTitle !== '기산 후퇴로 후 군영' ||
|
|
fiftyEighthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftyEighthCampState.availableDialogueIds.every((id) => id.startsWith('northern-fourth-')) ||
|
|
fiftyEighthCampState.availableVisitIds?.length !== 1 ||
|
|
!fiftyEighthCampState.availableVisitIds.every((id) => id.startsWith('northern-fourth-')) ||
|
|
fiftyEighthCampState.rosterCollection?.total < 22 ||
|
|
!fiftyEighthCampState.report?.bonds?.some((bond) => bond.id === 'zhuge-liang__jiang-wei_northern-fourth')
|
|
) {
|
|
throw new Error(`Expected Qishan retreat camp to expose Jiang Wei first-sortie aftermath events and northern-fourth bonds: ${JSON.stringify(fiftyEighthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-eighth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFiftyEighthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFiftyEighthProgressState?.activeTab !== 'progress' ||
|
|
postFiftyEighthProgressState.campaignProgress?.completedKnown !== 58 ||
|
|
postFiftyEighthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFiftyEighthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postFiftyEighthProgressState.campaignProgress?.latestBattleTitle !== '기산 후퇴로' ||
|
|
postFiftyEighthProgressState.campaignProgress?.nextBattleTitle !== '진창 공성전'
|
|
) {
|
|
throw new Error(`Expected post-fifty-eighth progress tab to complete the Qishan retreat and point toward Chencang: ${JSON.stringify(postFiftyEighthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fifty-eighth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const chencangSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!chencangSortieState?.sortieVisible ||
|
|
chencangSortieState.campaign?.step !== 'fifty-eighth-camp' ||
|
|
chencangSortieState.campTitle !== '기산 후퇴로 후 군영' ||
|
|
!String(chencangSortieState.sortiePlan?.objectiveLine ?? '').includes('진창 공성전') ||
|
|
!String(chencangSortieState.sortiePlan?.objectiveLine ?? '').includes('학소') ||
|
|
chencangSortieState.sortiePlan?.maxCount !== 7 ||
|
|
chencangSortieState.sortiePlan?.recommendedTotal !== 7 ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!chencangSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recommended) ||
|
|
chencangSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Chencang sortie to require Zhuge Liang, exclude Liu Bei, and recommend Jiang Wei, Wei Yan, terrain, cavalry, and logistics roles: ${JSON.stringify(chencangSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(chencangSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const chencangPriorityUnits = ['jiang-wei', 'wang-ping', 'ma-dai', 'wei-yan', 'huang-quan', 'fa-zheng'];
|
|
for (const unitId of chencangPriorityUnits) {
|
|
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.required && !chencangPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const chencangSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!chencangPriorityUnits.every((unitId) =>
|
|
chencangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!chencangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
chencangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
chencangSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
chencangSortieReadyState.sortiePlan?.recommendedSelectedCount < 7
|
|
) {
|
|
throw new Error(`Expected Chencang sortie to deploy Zhuge Liang, Jiang Wei, Wang Ping, Ma Dai, Wei Yan, Huang Quan, and Fa Zheng: ${JSON.stringify(chencangSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-ninth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-fifty-ninth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredFiftyNinthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'fifty-ninth-battle-chencang-siege';
|
|
});
|
|
if (enteredFiftyNinthBattle) {
|
|
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 === 'fifty-ninth-battle-chencang-siege' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-fifty-ninth-battle.png', fullPage: true });
|
|
|
|
const fiftyNinthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const fiftyNinthEnemies = fiftyNinthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const fiftyNinthAllies = fiftyNinthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const fiftyNinthEnemyBehaviors = new Set(fiftyNinthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
fiftyNinthBattleState.camera?.mapWidth !== 118 ||
|
|
fiftyNinthBattleState.camera?.mapHeight !== 100 ||
|
|
fiftyNinthBattleState.victoryConditionLabel !== '학소 진창 수비 압박과 왕쌍 추격 차단' ||
|
|
!fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'pressure-hao-zhao' && objective.label === '학소 진창 수비 압박') ||
|
|
!fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'halt-wang-shuang' && objective.label === '왕쌍 추격 차단') ||
|
|
!fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'chencang-gate' && objective.label === '진창 성문 압박') ||
|
|
!fiftyNinthBattleState.objectives?.some((objective) => objective.id === 'siege-camps' && objective.label === '공성 진영 유지') ||
|
|
fiftyNinthEnemies.length < 42 ||
|
|
!fiftyNinthEnemyBehaviors.has('aggressive') ||
|
|
!fiftyNinthEnemyBehaviors.has('guard') ||
|
|
!fiftyNinthEnemyBehaviors.has('hold') ||
|
|
!fiftyNinthEnemies.some((unit) => unit.id === 'northern-fifth-leader-hao-zhao') ||
|
|
!fiftyNinthEnemies.some((unit) => unit.id === 'northern-fifth-officer-wang-shuang') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'jiang-wei') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!fiftyNinthAllies.some((unit) => unit.id === 'fa-zheng') ||
|
|
fiftyNinthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected fifty-ninth battle to use the Chencang siege map, Hao Zhao/Wang Shuang objectives, selected Jiang Wei sortie, and mixed Wei AI: ${JSON.stringify(fiftyNinthBattleState)}`);
|
|
}
|
|
|
|
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 fiftyNinthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
fiftyNinthCampState?.campBattleId !== 'fifty-ninth-battle-chencang-siege' ||
|
|
fiftyNinthCampState.campaign?.step !== 'fifty-ninth-camp' ||
|
|
fiftyNinthCampState.campTitle !== '진창 공성전 후 군영' ||
|
|
fiftyNinthCampState.availableDialogueIds?.length !== 3 ||
|
|
!fiftyNinthCampState.availableDialogueIds.every((id) => id.startsWith('northern-fifth-')) ||
|
|
fiftyNinthCampState.availableVisitIds?.length !== 1 ||
|
|
!fiftyNinthCampState.availableVisitIds.every((id) => id.startsWith('northern-fifth-')) ||
|
|
fiftyNinthCampState.rosterCollection?.total < 22 ||
|
|
!fiftyNinthCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__fa-zheng_northern-fifth')
|
|
) {
|
|
throw new Error(`Expected Chencang aftermath camp to expose fifth northern campaign events and bonds: ${JSON.stringify(fiftyNinthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-fifty-ninth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postFiftyNinthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postFiftyNinthProgressState?.activeTab !== 'progress' ||
|
|
postFiftyNinthProgressState.campaignProgress?.completedKnown !== 59 ||
|
|
postFiftyNinthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postFiftyNinthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postFiftyNinthProgressState.campaignProgress?.latestBattleTitle !== '진창 공성전' ||
|
|
postFiftyNinthProgressState.campaignProgress?.nextBattleTitle !== '무도·음평 확보전'
|
|
) {
|
|
throw new Error(`Expected post-fifty-ninth progress tab to complete Chencang and point toward Wudu/Yinping: ${JSON.stringify(postFiftyNinthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-fifty-ninth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const wuduSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!wuduSortieState?.sortieVisible ||
|
|
wuduSortieState.campaign?.step !== 'fifty-ninth-camp' ||
|
|
wuduSortieState.campTitle !== '진창 공성전 후 군영' ||
|
|
!String(wuduSortieState.sortiePlan?.objectiveLine ?? '').includes('무도·음평 확보전') ||
|
|
!String(wuduSortieState.sortiePlan?.objectiveLine ?? '').includes('곽회') ||
|
|
wuduSortieState.sortiePlan?.maxCount !== 7 ||
|
|
wuduSortieState.sortiePlan?.recommendedTotal !== 8 ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!wuduSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
wuduSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Wudu/Yinping sortie to require Zhuge Liang, exclude Liu Bei, and recommend eight competing northern roles: ${JSON.stringify(wuduSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(wuduSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const wuduPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang'];
|
|
for (const unitId of wuduPriorityUnits) {
|
|
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.required && !wuduPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const wuduSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!wuduPriorityUnits.every((unitId) =>
|
|
wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
wuduSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) ||
|
|
wuduSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
wuduSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
wuduSortieReadyState.sortiePlan?.recommendedTotal !== 8
|
|
) {
|
|
throw new Error(`Expected Wudu/Yinping sortie to deploy Zhuge Liang, Jiang Wei, Wei Yan, Zhao Yun, Wang Ping, Huang Quan, and Ma Liang while benching one recommended cavalry role: ${JSON.stringify(wuduSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixtieth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-sixtieth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtiethBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixtieth-battle-wudu-yinping';
|
|
});
|
|
if (enteredSixtiethBattle) {
|
|
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 === 'sixtieth-battle-wudu-yinping' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixtieth-battle.png', fullPage: true });
|
|
|
|
const sixtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtiethEnemies = sixtiethBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtiethAllies = sixtiethBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const sixtiethEnemyBehaviors = new Set(sixtiethEnemies.map((unit) => unit.ai));
|
|
if (
|
|
sixtiethBattleState.camera?.mapWidth !== 120 ||
|
|
sixtiethBattleState.camera?.mapHeight !== 102 ||
|
|
sixtiethBattleState.victoryConditionLabel !== '곽회 구원로 차단과 무도·음평 확보' ||
|
|
!sixtiethBattleState.objectives?.some((objective) => objective.id === 'repel-guo-huai-relief' && objective.label === '곽회 구원로 차단') ||
|
|
!sixtiethBattleState.objectives?.some((objective) => objective.id === 'secure-wudu-guard' && objective.label === '무도 고개 수비 제압') ||
|
|
!sixtiethBattleState.objectives?.some((objective) => objective.id === 'secure-yinping-guard' && objective.label === '음평 물목 수비 제압') ||
|
|
!sixtiethBattleState.objectives?.some((objective) => objective.id === 'two-commandery-road' && objective.label === '두 고을 수레 길 확보') ||
|
|
!sixtiethBattleState.objectives?.some((objective) => objective.id === 'wudu-yinping-villages' && objective.label === '무도·음평 마을 안정') ||
|
|
sixtiethEnemies.length < 42 ||
|
|
!sixtiethEnemyBehaviors.has('aggressive') ||
|
|
!sixtiethEnemyBehaviors.has('guard') ||
|
|
!sixtiethEnemyBehaviors.has('hold') ||
|
|
!sixtiethEnemies.some((unit) => unit.id === 'northern-sixth-leader-guo-huai') ||
|
|
!sixtiethEnemies.some((unit) => unit.id === 'northern-sixth-officer-wudu') ||
|
|
!sixtiethEnemies.some((unit) => unit.id === 'northern-sixth-officer-yinping') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'jiang-wei') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!sixtiethAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
sixtiethAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
sixtiethAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixtieth battle to use the Wudu/Yinping map, Guo Huai objectives, a deliberately benched recommended cavalry role, and mixed Wei AI: ${JSON.stringify(sixtiethBattleState)}`);
|
|
}
|
|
|
|
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 sixtiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtiethCampState?.campBattleId !== 'sixtieth-battle-wudu-yinping' ||
|
|
sixtiethCampState.campaign?.step !== 'sixtieth-camp' ||
|
|
sixtiethCampState.campTitle !== '무도·음평 확보 후 군영' ||
|
|
sixtiethCampState.availableDialogueIds?.length !== 3 ||
|
|
!sixtiethCampState.availableDialogueIds.every((id) => id.startsWith('northern-sixth-')) ||
|
|
sixtiethCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtiethCampState.availableVisitIds.every((id) => id.startsWith('northern-sixth-')) ||
|
|
sixtiethCampState.rosterCollection?.total < 22 ||
|
|
!sixtiethCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__wei-yan_northern-sixth')
|
|
) {
|
|
throw new Error(`Expected Wudu/Yinping aftermath camp to expose sixth northern campaign events and bonds: ${JSON.stringify(sixtiethCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixtieth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtiethProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtiethProgressState?.activeTab !== 'progress' ||
|
|
postSixtiethProgressState.campaignProgress?.completedKnown !== 60 ||
|
|
postSixtiethProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtiethProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtiethProgressState.campaignProgress?.latestBattleTitle !== '무도·음평 확보전' ||
|
|
postSixtiethProgressState.campaignProgress?.nextBattleTitle !== '한중 우로 방어전'
|
|
) {
|
|
throw new Error(`Expected post-sixtieth progress tab to complete Wudu/Yinping and point toward Hanzhong rainy-pass defense: ${JSON.stringify(postSixtiethProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixtieth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const hanzhongRainSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanzhongRainSortieState?.sortieVisible ||
|
|
hanzhongRainSortieState.campaign?.step !== 'sixtieth-camp' ||
|
|
hanzhongRainSortieState.campTitle !== '무도·음평 확보 후 군영' ||
|
|
!String(hanzhongRainSortieState.sortiePlan?.objectiveLine ?? '').includes('한중 우로 방어전') ||
|
|
!String(hanzhongRainSortieState.sortiePlan?.objectiveLine ?? '').includes('보급선') ||
|
|
hanzhongRainSortieState.sortiePlan?.maxCount !== 7 ||
|
|
hanzhongRainSortieState.sortiePlan?.recommendedTotal !== 9 ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
hanzhongRainSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Hanzhong rainy-pass sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing defense/logistics roles: ${JSON.stringify(hanzhongRainSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(hanzhongRainSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const hanzhongRainPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'ma-liang', 'wei-yan'];
|
|
for (const unitId of hanzhongRainPriorityUnits) {
|
|
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.required && !hanzhongRainPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const hanzhongRainSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!hanzhongRainPriorityUnits.every((unitId) =>
|
|
hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) ||
|
|
hanzhongRainSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) ||
|
|
hanzhongRainSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
hanzhongRainSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
hanzhongRainSortieReadyState.sortiePlan?.recommendedTotal !== 9
|
|
) {
|
|
throw new Error(`Expected Hanzhong rainy-pass sortie to deploy Zhuge Liang plus six logistics/road officers while benching two recommended cavalry roles: ${JSON.stringify(hanzhongRainSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-first-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-sixty-first-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtyFirstBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixty-first-battle-hanzhong-rain-defense';
|
|
});
|
|
if (enteredSixtyFirstBattle) {
|
|
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 === 'sixty-first-battle-hanzhong-rain-defense' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixty-first-battle.png', fullPage: true });
|
|
|
|
const sixtyFirstBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtyFirstEnemies = sixtyFirstBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtyFirstAllies = sixtyFirstBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const sixtyFirstEnemyBehaviors = new Set(sixtyFirstEnemies.map((unit) => unit.ai));
|
|
if (
|
|
sixtyFirstBattleState.camera?.mapWidth !== 122 ||
|
|
sixtyFirstBattleState.camera?.mapHeight !== 104 ||
|
|
sixtyFirstBattleState.victoryConditionLabel !== '한중 북문과 두 고을 보급선 방어' ||
|
|
!sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'repel-cao-zhen-main' && objective.label === '조진 본대 격퇴') ||
|
|
!sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'check-sima-yi-column' && objective.label === '사마의 별동대 견제') ||
|
|
!sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'hold-wudu-yinping-supply' && objective.label === '무도·음평 보급선 방어') ||
|
|
!sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'hanzhong-granary-camps' && objective.label === '한중 창고 진영 유지') ||
|
|
!sixtyFirstBattleState.objectives?.some((objective) => objective.id === 'rain-pass-roads' && objective.label === '빗길 수레길 확보') ||
|
|
sixtyFirstEnemies.length < 42 ||
|
|
!sixtyFirstEnemyBehaviors.has('aggressive') ||
|
|
!sixtyFirstEnemyBehaviors.has('guard') ||
|
|
!sixtyFirstEnemyBehaviors.has('hold') ||
|
|
!sixtyFirstEnemies.some((unit) => unit.id === 'northern-seventh-leader-cao-zhen') ||
|
|
!sixtyFirstEnemies.some((unit) => unit.id === 'northern-seventh-officer-sima-yi') ||
|
|
!sixtyFirstEnemies.some((unit) => unit.id === 'northern-seventh-officer-supply') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'jiang-wei') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'li-yan') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
!sixtyFirstAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
sixtyFirstAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
sixtyFirstAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
sixtyFirstAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixty-first battle to use the Hanzhong rainy-pass map, Cao Zhen/Sima Yi objectives, logistics-heavy selected sortie, and mixed Wei AI: ${JSON.stringify(sixtyFirstBattleState)}`);
|
|
}
|
|
|
|
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 sixtyFirstCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtyFirstCampState?.campBattleId !== 'sixty-first-battle-hanzhong-rain-defense' ||
|
|
sixtyFirstCampState.campaign?.step !== 'sixty-first-camp' ||
|
|
sixtyFirstCampState.campTitle !== '한중 우로 방어 후 군영' ||
|
|
sixtyFirstCampState.availableDialogueIds?.length !== 3 ||
|
|
!sixtyFirstCampState.availableDialogueIds.every((id) => id.startsWith('northern-seventh-')) ||
|
|
sixtyFirstCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtyFirstCampState.availableVisitIds.every((id) => id.startsWith('northern-seventh-')) ||
|
|
sixtyFirstCampState.rosterCollection?.total < 22 ||
|
|
!sixtyFirstCampState.report?.bonds?.some((bond) => bond.id === 'li-yan__huang-quan_northern-seventh')
|
|
) {
|
|
throw new Error(`Expected Hanzhong rainy-pass aftermath camp to expose seventh northern campaign events and bonds: ${JSON.stringify(sixtyFirstCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-first-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtyFirstProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtyFirstProgressState?.activeTab !== 'progress' ||
|
|
postSixtyFirstProgressState.campaignProgress?.completedKnown !== 61 ||
|
|
postSixtyFirstProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtyFirstProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtyFirstProgressState.campaignProgress?.latestBattleTitle !== '한중 우로 방어전' ||
|
|
postSixtyFirstProgressState.campaignProgress?.nextBattleTitle !== '기산 재출정전'
|
|
) {
|
|
throw new Error(`Expected post-sixty-first progress tab to complete Hanzhong rainy-pass defense and point toward Qishan renewed offensive: ${JSON.stringify(postSixtyFirstProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixty-first-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const qishanRenewedSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!qishanRenewedSortieState?.sortieVisible ||
|
|
qishanRenewedSortieState.campaign?.step !== 'sixty-first-camp' ||
|
|
qishanRenewedSortieState.campTitle !== '한중 우로 방어 후 군영' ||
|
|
!String(qishanRenewedSortieState.sortiePlan?.objectiveLine ?? '').includes('기산 재출정전') ||
|
|
!String(qishanRenewedSortieState.sortiePlan?.objectiveLine ?? '').includes('노성 지연선') ||
|
|
qishanRenewedSortieState.sortiePlan?.maxCount !== 7 ||
|
|
qishanRenewedSortieState.sortiePlan?.recommendedTotal !== 9 ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
|
|
!qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
qishanRenewedSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Qishan renewed sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing offensive/return-line roles: ${JSON.stringify(qishanRenewedSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(qishanRenewedSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const qishanRenewedPriorityUnits = ['jiang-wei', 'wei-yan', 'zhao-yun', 'wang-ping', 'huang-quan', 'ma-liang'];
|
|
for (const unitId of qishanRenewedPriorityUnits) {
|
|
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.required && !qishanRenewedPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const qishanRenewedSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!qishanRenewedPriorityUnits.every((unitId) =>
|
|
qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.selected) ||
|
|
qishanRenewedSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) ||
|
|
qishanRenewedSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
qishanRenewedSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
qishanRenewedSortieReadyState.sortiePlan?.recommendedTotal !== 9
|
|
) {
|
|
throw new Error(`Expected Qishan renewed sortie to deploy Zhuge Liang plus six forward/return-line officers while benching Li Yan and Ma Dai: ${JSON.stringify(qishanRenewedSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-second-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
await page.screenshot({ path: 'dist/verification-sixty-second-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtySecondBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixty-second-battle-qishan-renewed-offensive';
|
|
});
|
|
if (enteredSixtySecondBattle) {
|
|
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 === 'sixty-second-battle-qishan-renewed-offensive' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixty-second-battle.png', fullPage: true });
|
|
|
|
const sixtySecondBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtySecondEnemies = sixtySecondBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtySecondAllies = sixtySecondBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const sixtySecondEnemyBehaviors = new Set(sixtySecondEnemies.map((unit) => unit.ai));
|
|
if (
|
|
sixtySecondBattleState.camera?.mapWidth !== 124 ||
|
|
sixtySecondBattleState.camera?.mapHeight !== 106 ||
|
|
sixtySecondBattleState.mapTextureKey !== 'battle-map-sixty-second' ||
|
|
sixtySecondBattleState.mapTextureReady !== true ||
|
|
sixtySecondBattleState.mapBackgroundReady !== true ||
|
|
sixtySecondBattleState.victoryConditionLabel !== '노성 지연선 돌파와 기산 보급로 전진' ||
|
|
!sixtySecondBattleState.objectives?.some((objective) => objective.id === 'press-sima-yi-delay-line' && objective.label === '사마의 지연선 돌파') ||
|
|
!sixtySecondBattleState.objectives?.some((objective) => objective.id === 'check-zhang-he-pursuit' && objective.label === '장합 추격대 저지') ||
|
|
!sixtySecondBattleState.objectives?.some((objective) => objective.id === 'break-guo-huai-flank' && objective.label === '곽회 측면 차단 해제') ||
|
|
!sixtySecondBattleState.objectives?.some((objective) => objective.id === 'qishan-supply-camps' && objective.label === '기산 보급 진영 유지') ||
|
|
!sixtySecondBattleState.objectives?.some((objective) => objective.id === 'lucheng-front-villages' && objective.label === '노성 앞 마을 안심') ||
|
|
sixtySecondEnemies.length < 42 ||
|
|
!sixtySecondEnemyBehaviors.has('aggressive') ||
|
|
!sixtySecondEnemyBehaviors.has('guard') ||
|
|
!sixtySecondEnemyBehaviors.has('hold') ||
|
|
!sixtySecondEnemies.some((unit) => unit.id === 'northern-eighth-leader-sima-yi') ||
|
|
!sixtySecondEnemies.some((unit) => unit.id === 'northern-eighth-officer-zhang-he') ||
|
|
!sixtySecondEnemies.some((unit) => unit.id === 'northern-eighth-officer-guo-huai') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'jiang-wei') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!sixtySecondAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
sixtySecondAllies.some((unit) => unit.id === 'li-yan') ||
|
|
sixtySecondAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
sixtySecondAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixty-second battle to use the Qishan renewed-offensive map, Sima Yi/Zhang He/Guo Huai objectives, selected forward/return-line officers, and mixed Wei AI: ${JSON.stringify(sixtySecondBattleState)}`);
|
|
}
|
|
|
|
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 sixtySecondCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtySecondCampState?.campBattleId !== 'sixty-second-battle-qishan-renewed-offensive' ||
|
|
sixtySecondCampState.campaign?.step !== 'sixty-second-camp' ||
|
|
sixtySecondCampState.campTitle !== '기산 재출정 후 군영' ||
|
|
sixtySecondCampState.availableDialogueIds?.length !== 3 ||
|
|
!sixtySecondCampState.availableDialogueIds.every((id) => id.startsWith('northern-eighth-')) ||
|
|
sixtySecondCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtySecondCampState.availableVisitIds.every((id) => id.startsWith('northern-eighth-')) ||
|
|
sixtySecondCampState.rosterCollection?.total < 22 ||
|
|
!sixtySecondCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__wei-yan_northern-eighth')
|
|
) {
|
|
throw new Error(`Expected Qishan renewed-offensive aftermath camp to expose eighth northern campaign events and bonds: ${JSON.stringify(sixtySecondCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-second-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtySecondProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtySecondProgressState?.activeTab !== 'progress' ||
|
|
postSixtySecondProgressState.campaignProgress?.completedKnown !== 62 ||
|
|
postSixtySecondProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtySecondProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtySecondProgressState.campaignProgress?.latestBattleTitle !== '기산 재출정전' ||
|
|
postSixtySecondProgressState.campaignProgress?.nextBattleTitle !== '노성 추격전'
|
|
) {
|
|
throw new Error(`Expected post-sixty-second progress tab to complete Qishan renewed offensive and point toward Lucheng pursuit: ${JSON.stringify(postSixtySecondProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixty-second-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const luchengPursuitSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!luchengPursuitSortieState?.sortieVisible ||
|
|
luchengPursuitSortieState.campaign?.step !== 'sixty-second-camp' ||
|
|
luchengPursuitSortieState.campTitle !== '기산 재출정 후 군영' ||
|
|
!String(luchengPursuitSortieState.sortiePlan?.objectiveLine ?? '').includes('노성 추격전') ||
|
|
!String(luchengPursuitSortieState.sortiePlan?.objectiveLine ?? '').includes('회수 표식') ||
|
|
luchengPursuitSortieState.sortiePlan?.maxCount !== 7 ||
|
|
luchengPursuitSortieState.sortiePlan?.recommendedTotal !== 9 ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
!luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
luchengPursuitSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Lucheng pursuit sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine competing pursuit/return/supply roles: ${JSON.stringify(luchengPursuitSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(luchengPursuitSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const luchengPursuitPriorityUnits = ['jiang-wei', 'zhao-yun', 'wei-yan', 'wang-ping', 'huang-quan', 'li-yan'];
|
|
for (const unitId of luchengPursuitPriorityUnits) {
|
|
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.required && !luchengPursuitPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const luchengPursuitSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!luchengPursuitPriorityUnits.every((unitId) =>
|
|
luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) ||
|
|
luchengPursuitSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.selected) ||
|
|
luchengPursuitSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
luchengPursuitSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
luchengPursuitSortieReadyState.sortiePlan?.recommendedTotal !== 9
|
|
) {
|
|
throw new Error(`Expected Lucheng pursuit sortie to deploy Zhuge Liang plus six pursuit/return/supply officers while benching Ma Liang and Ma Dai: ${JSON.stringify(luchengPursuitSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-third-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
for (let i = 0; i < 12; i += 1) {
|
|
const storyState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (
|
|
storyState?.requestedBackground === 'story-lucheng-pursuit' &&
|
|
storyState?.background?.startsWith('story-lucheng-pursuit')
|
|
) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
const luchengPursuitStoryState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (
|
|
luchengPursuitStoryState?.requestedBackground !== 'story-lucheng-pursuit' ||
|
|
!luchengPursuitStoryState?.background?.startsWith('story-lucheng-pursuit') ||
|
|
luchengPursuitStoryState?.backgroundReady !== true
|
|
) {
|
|
throw new Error(`Expected Lucheng pursuit story to lazy-load its new generated background: ${JSON.stringify(luchengPursuitStoryState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-third-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtyThirdBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixty-third-battle-lucheng-pursuit';
|
|
});
|
|
if (enteredSixtyThirdBattle) {
|
|
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 === 'sixty-third-battle-lucheng-pursuit' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixty-third-battle.png', fullPage: true });
|
|
|
|
const sixtyThirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtyThirdEnemies = sixtyThirdBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtyThirdAllies = sixtyThirdBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const sixtyThirdEnemyBehaviors = new Set(sixtyThirdEnemies.map((unit) => unit.ai));
|
|
if (
|
|
sixtyThirdBattleState.camera?.mapWidth !== 126 ||
|
|
sixtyThirdBattleState.camera?.mapHeight !== 108 ||
|
|
sixtyThirdBattleState.mapTextureKey !== 'battle-map-sixty-third' ||
|
|
sixtyThirdBattleState.mapTextureReady !== true ||
|
|
sixtyThirdBattleState.mapBackgroundReady !== true ||
|
|
sixtyThirdBattleState.victoryConditionLabel !== '노성 추격로 확보와 회수 표식 유지' ||
|
|
!sixtyThirdBattleState.objectives?.some((objective) => objective.id === 'break-sima-yi-lucheng-defense' && objective.label === '사마의 노성 방어 돌파') ||
|
|
!sixtyThirdBattleState.objectives?.some((objective) => objective.id === 'stop-zhang-he-return-raid' && objective.label === '장합 회수선 추격 저지') ||
|
|
!sixtyThirdBattleState.objectives?.some((objective) => objective.id === 'silence-guo-huai-beacons' && objective.label === '곽회 봉화 차단') ||
|
|
!sixtyThirdBattleState.objectives?.some((objective) => objective.id === 'hold-return-supply-camps' && objective.label === '회수 보급 진영 유지') ||
|
|
!sixtyThirdBattleState.objectives?.some((objective) => objective.id === 'reassure-lucheng-villages' && objective.label === '노성 주변 마을 안심') ||
|
|
sixtyThirdEnemies.length < 42 ||
|
|
!sixtyThirdEnemyBehaviors.has('aggressive') ||
|
|
!sixtyThirdEnemyBehaviors.has('guard') ||
|
|
!sixtyThirdEnemyBehaviors.has('hold') ||
|
|
!sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-leader-sima-yi' && unit.textureBase === 'unit-sima-yi') ||
|
|
!sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-officer-zhang-he') ||
|
|
!sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-officer-guo-huai') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'zhuge-liang' && unit.textureBase === 'unit-zhuge-liang') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'jiang-wei' && unit.textureBase === 'unit-jiang-wei') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'zhao-yun' && unit.textureBase === 'unit-zhao-yun') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!sixtyThirdAllies.some((unit) => unit.id === 'li-yan') ||
|
|
sixtyThirdAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
sixtyThirdAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
sixtyThirdAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixty-third battle to use the Lucheng pursuit map, Sima Yi/Zhang He/Guo Huai objectives, selected pursuit/return/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyThirdBattleState)}`);
|
|
}
|
|
|
|
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 sixtyThirdCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtyThirdCampState?.campBattleId !== 'sixty-third-battle-lucheng-pursuit' ||
|
|
sixtyThirdCampState.campaign?.step !== 'sixty-third-camp' ||
|
|
sixtyThirdCampState.campTitle !== '노성 추격 후 군영' ||
|
|
sixtyThirdCampState.availableDialogueIds?.length !== 3 ||
|
|
!sixtyThirdCampState.availableDialogueIds.every((id) => id.startsWith('northern-ninth-')) ||
|
|
sixtyThirdCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtyThirdCampState.availableVisitIds.every((id) => id.startsWith('northern-ninth-')) ||
|
|
sixtyThirdCampState.rosterCollection?.total < 22 ||
|
|
!sixtyThirdCampState.report?.bonds?.some((bond) => bond.id === 'jiang-wei__zhao-yun_northern-ninth')
|
|
) {
|
|
throw new Error(`Expected Lucheng pursuit aftermath camp to expose ninth northern campaign events and bonds: ${JSON.stringify(sixtyThirdCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-third-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtyThirdProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtyThirdProgressState?.activeTab !== 'progress' ||
|
|
postSixtyThirdProgressState.campaignProgress?.completedKnown !== 63 ||
|
|
postSixtyThirdProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtyThirdProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtyThirdProgressState.campaignProgress?.latestBattleTitle !== '노성 추격전' ||
|
|
postSixtyThirdProgressState.campaignProgress?.nextBattleTitle !== '위수 진영전'
|
|
) {
|
|
throw new Error(`Expected post-sixty-third progress tab to complete Lucheng pursuit and point toward Weishui camps: ${JSON.stringify(postSixtyThirdProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixty-third-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const weishuiCampsSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!weishuiCampsSortieState?.sortieVisible ||
|
|
weishuiCampsSortieState.campaign?.step !== 'sixty-third-camp' ||
|
|
weishuiCampsSortieState.campTitle !== '노성 추격 후 군영' ||
|
|
!String(weishuiCampsSortieState.sortiePlan?.objectiveLine ?? '').includes('위수 진영전') ||
|
|
!String(weishuiCampsSortieState.sortiePlan?.objectiveLine ?? '').includes('봉화 차단') ||
|
|
weishuiCampsSortieState.sortiePlan?.maxCount !== 7 ||
|
|
weishuiCampsSortieState.sortiePlan?.recommendedTotal !== 9 ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
weishuiCampsSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Weishui camps sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine river/camp/supply roles: ${JSON.stringify(weishuiCampsSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(weishuiCampsSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const weishuiCampsPriorityUnits = ['jiang-wei', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan', 'ma-dai'];
|
|
for (const unitId of weishuiCampsPriorityUnits) {
|
|
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.required && !weishuiCampsPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const weishuiCampsSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!weishuiCampsPriorityUnits.every((unitId) =>
|
|
weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) ||
|
|
weishuiCampsSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) ||
|
|
weishuiCampsSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
weishuiCampsSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
weishuiCampsSortieReadyState.sortiePlan?.recommendedTotal !== 9
|
|
) {
|
|
throw new Error(`Expected Weishui camps sortie to deploy Zhuge Liang plus six river/camp/supply officers while benching Zhao Yun and Ma Liang: ${JSON.stringify(weishuiCampsSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-fourth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
for (let i = 0; i < 12; i += 1) {
|
|
const storyState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (
|
|
storyState?.requestedBackground === 'story-weishui-camps' &&
|
|
storyState?.background?.startsWith('story-weishui-camps')
|
|
) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
const weishuiCampsStoryState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (
|
|
weishuiCampsStoryState?.requestedBackground !== 'story-weishui-camps' ||
|
|
!weishuiCampsStoryState?.background?.startsWith('story-weishui-camps') ||
|
|
weishuiCampsStoryState?.backgroundReady !== true ||
|
|
weishuiCampsStoryState?.portraitKey !== 'zhugeLiang' ||
|
|
!weishuiCampsStoryState?.portraitTextureKey?.startsWith('portrait-zhuge-liang')
|
|
) {
|
|
throw new Error(`Expected Weishui camps story to lazy-load its new generated background and show Zhuge Liang portrait by speaker fallback: ${JSON.stringify(weishuiCampsStoryState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-fourth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtyFourthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixty-fourth-battle-weishui-camps';
|
|
});
|
|
if (enteredSixtyFourthBattle) {
|
|
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 === 'sixty-fourth-battle-weishui-camps' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixty-fourth-battle.png', fullPage: true });
|
|
|
|
const sixtyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtyFourthEnemies = sixtyFourthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtyFourthAllies = sixtyFourthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const sixtyFourthEnemyBehaviors = new Set(sixtyFourthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
sixtyFourthBattleState.camera?.mapWidth !== 128 ||
|
|
sixtyFourthBattleState.camera?.mapHeight !== 110 ||
|
|
sixtyFourthBattleState.mapTextureKey !== 'battle-map-sixty-fourth' ||
|
|
sixtyFourthBattleState.mapTextureReady !== true ||
|
|
sixtyFourthBattleState.mapBackgroundReady !== true ||
|
|
sixtyFourthBattleState.victoryConditionLabel !== '위수 남안 진영 확보와 봉화 차단' ||
|
|
!sixtyFourthBattleState.objectives?.some((objective) => objective.id === 'break-sima-yi-weishui-camps' && objective.label === '사마의 위수 본영 압박') ||
|
|
!sixtyFourthBattleState.objectives?.some((objective) => objective.id === 'stop-zhang-he-crossing-raid' && objective.label === '장합 물목 추격 저지') ||
|
|
!sixtyFourthBattleState.objectives?.some((objective) => objective.id === 'silence-guo-huai-weishui-beacons' && objective.label === '곽회 위수 봉화 차단') ||
|
|
!sixtyFourthBattleState.objectives?.some((objective) => objective.id === 'hold-weishui-supply-camps' && objective.label === '위수 보급 진영 확보') ||
|
|
!sixtyFourthBattleState.objectives?.some((objective) => objective.id === 'reassure-river-villages' && objective.label === '강가 마을 안심') ||
|
|
sixtyFourthEnemies.length < 42 ||
|
|
!sixtyFourthEnemyBehaviors.has('aggressive') ||
|
|
!sixtyFourthEnemyBehaviors.has('guard') ||
|
|
!sixtyFourthEnemyBehaviors.has('hold') ||
|
|
!sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-leader-sima-yi' && unit.combatPortraitKey === 'portrait-sima-yi' && unit.textureBase === 'unit-sima-yi') ||
|
|
!sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-officer-zhang-he') ||
|
|
!sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-officer-guo-huai') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'zhuge-liang' && unit.combatPortraitKey === 'portrait-zhuge-liang' && unit.textureBase === 'unit-zhuge-liang') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'jiang-wei' && unit.combatPortraitKey === 'portrait-jiang-wei' && unit.textureBase === 'unit-jiang-wei') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'li-yan') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!sixtyFourthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
sixtyFourthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
sixtyFourthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
sixtyFourthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixty-fourth battle to use the Weishui camps map, Sima Yi/Zhang He/Guo Huai objectives, selected river/camp/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyFourthBattleState)}`);
|
|
}
|
|
|
|
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 sixtyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtyFourthCampState?.campBattleId !== 'sixty-fourth-battle-weishui-camps' ||
|
|
sixtyFourthCampState.campaign?.step !== 'sixty-fourth-camp' ||
|
|
sixtyFourthCampState.campTitle !== '위수 진영전 후 군영' ||
|
|
sixtyFourthCampState.availableDialogueIds?.length !== 2 ||
|
|
!sixtyFourthCampState.availableDialogueIds.every((id) => id.startsWith('northern-tenth-')) ||
|
|
sixtyFourthCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtyFourthCampState.availableVisitIds.every((id) => id.startsWith('northern-tenth-')) ||
|
|
sixtyFourthCampState.rosterCollection?.total < 22 ||
|
|
!sixtyFourthCampState.report?.bonds?.some((bond) => bond.id === 'zhuge-liang__huang-quan_northern-tenth')
|
|
) {
|
|
throw new Error(`Expected Weishui camps aftermath camp to expose tenth northern campaign events and bonds: ${JSON.stringify(sixtyFourthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-fourth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtyFourthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtyFourthProgressState?.activeTab !== 'progress' ||
|
|
postSixtyFourthProgressState.campaignProgress?.completedKnown !== 64 ||
|
|
postSixtyFourthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtyFourthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtyFourthProgressState.campaignProgress?.latestBattleTitle !== '위수 진영전' ||
|
|
postSixtyFourthProgressState.campaignProgress?.nextBattleTitle !== '위수 북안 압박전'
|
|
) {
|
|
throw new Error(`Expected post-sixty-fourth progress tab to complete Weishui camps and point toward Weishui north bank pressure: ${JSON.stringify(postSixtyFourthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixty-fourth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1120, 38);
|
|
await page.waitForTimeout(180);
|
|
const northbankSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!northbankSortieState?.sortieVisible ||
|
|
northbankSortieState.campaign?.step !== 'sixty-fourth-camp' ||
|
|
northbankSortieState.campTitle !== '위수 진영전 후 군영' ||
|
|
!String(northbankSortieState.sortiePlan?.objectiveLine ?? '').includes('위수 북안 압박전') ||
|
|
!String(northbankSortieState.sortiePlan?.objectiveLine ?? '').includes('재추격 저지') ||
|
|
northbankSortieState.sortiePlan?.maxCount !== 7 ||
|
|
northbankSortieState.sortiePlan?.recommendedTotal !== 9 ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'jiang-wei' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'ma-dai' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'li-yan' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) ||
|
|
!northbankSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) ||
|
|
northbankSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Weishui north bank sortie to require Zhuge Liang, exclude Liu Bei, and recommend nine low-road/flank/supply roles: ${JSON.stringify(northbankSortieState)}`);
|
|
}
|
|
assertSortieTacticalRoster(northbankSortieState, [
|
|
'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian', 'zhao-yun', 'zhuge-liang', 'ma-liang', 'yi-ji', 'gong-zhi', 'huang-zhong', 'wei-yan', 'pang-tong', 'fa-zheng', 'wu-yi', 'yan-yan', 'li-yan', 'huang-quan', 'ma-chao', 'ma-dai', 'wang-ping', 'jiang-wei'
|
|
]);
|
|
|
|
const northbankPriorityUnits = ['jiang-wei', 'ma-dai', 'wang-ping', 'huang-quan', 'li-yan', 'wei-yan'];
|
|
for (const unitId of northbankPriorityUnits) {
|
|
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.required && !northbankPriorityUnits.includes(unit.id)
|
|
);
|
|
if (removable) {
|
|
await clickSortieRosterUnit(page, removable.id);
|
|
}
|
|
await clickSortieRosterUnit(page, unitId);
|
|
}
|
|
}
|
|
|
|
const northbankSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!northbankPriorityUnits.every((unitId) =>
|
|
northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) ||
|
|
northbankSortieReadyState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.selected) ||
|
|
northbankSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
northbankSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
northbankSortieReadyState.sortiePlan?.recommendedTotal !== 9
|
|
) {
|
|
throw new Error(`Expected Weishui north bank sortie to deploy Zhuge Liang plus six low-road/flank/supply officers while benching Zhao Yun and Ma Liang: ${JSON.stringify(northbankSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-fifth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
for (let i = 0; i < 12; i += 1) {
|
|
const storyState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (
|
|
storyState?.requestedBackground === 'story-weishui-northbank' &&
|
|
storyState?.background?.startsWith('story-weishui-northbank')
|
|
) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
const northbankStoryState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
const northbankExpectedPortraits = {
|
|
'sixty-fifth-northbank-council': ['zhugeLiang', 'portrait-zhuge-liang'],
|
|
'sixty-fifth-forward-ledger': ['huangQuan', 'portrait-huang-quan'],
|
|
'sixty-fifth-rearguard-threat': ['jiangWei', 'portrait-jiang-wei']
|
|
};
|
|
const northbankExpectedPortrait = northbankExpectedPortraits[northbankStoryState?.currentPageId];
|
|
if (
|
|
northbankStoryState?.requestedBackground !== 'story-weishui-northbank' ||
|
|
!northbankStoryState?.background?.startsWith('story-weishui-northbank') ||
|
|
northbankStoryState?.backgroundReady !== true ||
|
|
!northbankExpectedPortrait ||
|
|
northbankStoryState?.portraitKey !== northbankExpectedPortrait[0] ||
|
|
!northbankStoryState?.portraitTextureKey?.startsWith(northbankExpectedPortrait[1])
|
|
) {
|
|
throw new Error(`Expected Weishui north bank story to lazy-load its new generated background and show the current speaker portrait by fallback: ${JSON.stringify(northbankStoryState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-fifth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtyFifthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixty-fifth-battle-weishui-northbank';
|
|
});
|
|
if (enteredSixtyFifthBattle) {
|
|
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 === 'sixty-fifth-battle-weishui-northbank' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixty-fifth-battle.png', fullPage: true });
|
|
|
|
const sixtyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtyFifthEnemies = sixtyFifthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtyFifthAllies = sixtyFifthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
const sixtyFifthEnemyBehaviors = new Set(sixtyFifthEnemies.map((unit) => unit.ai));
|
|
if (
|
|
sixtyFifthBattleState.camera?.mapWidth !== 130 ||
|
|
sixtyFifthBattleState.camera?.mapHeight !== 112 ||
|
|
sixtyFifthBattleState.mapTextureKey !== 'battle-map-sixty-fifth' ||
|
|
sixtyFifthBattleState.mapTextureReady !== true ||
|
|
sixtyFifthBattleState.mapBackgroundReady !== true ||
|
|
sixtyFifthBattleState.victoryConditionLabel !== '위수 북안 전진 진영 압박과 재추격 저지' ||
|
|
!sixtyFifthBattleState.objectives?.some((objective) => objective.id === 'break-sima-yi-northbank-delay-line' && objective.label === '사마의 북안 지연선 압박') ||
|
|
!sixtyFifthBattleState.objectives?.some((objective) => objective.id === 'stop-zhang-he-rearguard-raid' && objective.label === '장합 재추격대 저지') ||
|
|
!sixtyFifthBattleState.objectives?.some((objective) => objective.id === 'silence-guo-huai-northbank-beacons' && objective.label === '곽회 북안 봉화 차단') ||
|
|
!sixtyFifthBattleState.objectives?.some((objective) => objective.id === 'hold-northbank-forward-camps' && objective.label === '북안 전진 진영 확보') ||
|
|
!sixtyFifthBattleState.objectives?.some((objective) => objective.id === 'reassure-northbank-villages' && objective.label === '강북 마을 안심') ||
|
|
sixtyFifthEnemies.length < 42 ||
|
|
!sixtyFifthEnemyBehaviors.has('aggressive') ||
|
|
!sixtyFifthEnemyBehaviors.has('guard') ||
|
|
!sixtyFifthEnemyBehaviors.has('hold') ||
|
|
!sixtyFifthEnemies.some((unit) => unit.id === 'northern-eleventh-leader-sima-yi' && unit.combatPortraitKey === 'portrait-sima-yi' && unit.textureBase === 'unit-sima-yi') ||
|
|
!sixtyFifthEnemies.some((unit) => unit.id === 'northern-eleventh-officer-zhang-he') ||
|
|
!sixtyFifthEnemies.some((unit) => unit.id === 'northern-eleventh-officer-guo-huai') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'zhuge-liang' && unit.combatPortraitKey === 'portrait-zhuge-liang' && unit.textureBase === 'unit-zhuge-liang') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'jiang-wei' && unit.combatPortraitKey === 'portrait-jiang-wei' && unit.textureBase === 'unit-jiang-wei') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'ma-dai') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'wang-ping') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'huang-quan') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'li-yan') ||
|
|
!sixtyFifthAllies.some((unit) => unit.id === 'wei-yan') ||
|
|
!unitUsesTexture(sixtyFifthBattleState, 'ma-dai', 'unit-ma-dai') ||
|
|
!unitUsesTexture(sixtyFifthBattleState, 'wang-ping', 'unit-wang-ping') ||
|
|
!unitUsesTexture(sixtyFifthBattleState, 'huang-quan', 'unit-huang-quan') ||
|
|
!unitUsesTexture(sixtyFifthBattleState, 'li-yan', 'unit-li-yan') ||
|
|
!unitUsesTexture(sixtyFifthBattleState, 'wei-yan', 'unit-wei-yan') ||
|
|
sixtyFifthAllies.some((unit) => unit.id === 'zhao-yun') ||
|
|
sixtyFifthAllies.some((unit) => unit.id === 'ma-liang') ||
|
|
sixtyFifthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected sixty-fifth battle to use the Weishui north bank map, Sima Yi/Zhang He/Guo Huai objectives, selected low-road/flank/supply officers, and mixed Wei AI: ${JSON.stringify(sixtyFifthBattleState)}`);
|
|
}
|
|
|
|
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 sixtyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtyFifthCampState?.campBattleId !== 'sixty-fifth-battle-weishui-northbank' ||
|
|
sixtyFifthCampState.campaign?.step !== 'sixty-fifth-camp' ||
|
|
sixtyFifthCampState.campTitle !== '위수 북안 압박 후 군영' ||
|
|
sixtyFifthCampState.availableDialogueIds?.length !== 2 ||
|
|
!sixtyFifthCampState.availableDialogueIds.every((id) => id.startsWith('northern-eleventh-')) ||
|
|
sixtyFifthCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtyFifthCampState.availableVisitIds.every((id) => id.startsWith('northern-eleventh-')) ||
|
|
sixtyFifthCampState.rosterCollection?.total < 22 ||
|
|
!sixtyFifthCampState.report?.bonds?.some((bond) => bond.id === 'zhuge-liang__li-yan_northern-eleventh')
|
|
) {
|
|
throw new Error(`Expected Weishui north bank aftermath camp to expose eleventh northern campaign events and bonds: ${JSON.stringify(sixtyFifthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-fifth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtyFifthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtyFifthProgressState.campaignProgress?.completedKnown !== 65 ||
|
|
postSixtyFifthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtyFifthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtyFifthProgressState.campaignProgress?.latestBattleTitle !== '위수 북안 압박전' ||
|
|
postSixtyFifthProgressState.campaignProgress?.nextBattleTitle !== '오장원 최종전'
|
|
) {
|
|
throw new Error(`Expected post-sixty-fifth progress tab to complete Weishui north bank pressure and point toward the Wuzhang finale: ${JSON.stringify(postSixtyFifthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixty-fifth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1160, 38);
|
|
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortieVisible === true);
|
|
const wuzhangPriorityUnits = ['zhuge-liang', 'jiang-wei', 'wang-ping', 'ma-dai', 'huang-quan', 'li-yan', 'wei-yan'];
|
|
const wuzhangSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
!wuzhangPriorityUnits.every((unitId) =>
|
|
wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
|
) ||
|
|
!wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) ||
|
|
wuzhangSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') ||
|
|
wuzhangSortieReadyState.sortiePlan?.selectedCount !== 7 ||
|
|
wuzhangSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 ||
|
|
wuzhangSortieReadyState.sortiePlan?.recommendedTotal !== 9
|
|
) {
|
|
throw new Error(`Expected Wuzhang finale sortie to keep Zhuge Liang plus inherited northern campaign core officers selected: ${JSON.stringify(wuzhangSortieReadyState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-sixth-sortie.png', fullPage: true });
|
|
|
|
await page.mouse.click(1068, 646);
|
|
await waitForStoryReady(page);
|
|
for (let i = 0; i < 12; i += 1) {
|
|
const storyState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (storyState?.currentPageId?.startsWith('sixty-sixth-') && storyState?.backgroundReady === true) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
const wuzhangStoryState = await page.evaluate(() =>
|
|
window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.()
|
|
);
|
|
if (
|
|
!wuzhangStoryState?.currentPageId?.startsWith('sixty-sixth-') ||
|
|
!['story-weishui-northbank', 'story-northern', 'story-hanzhong-rain'].includes(wuzhangStoryState?.requestedBackground) ||
|
|
wuzhangStoryState?.backgroundReady !== true
|
|
) {
|
|
throw new Error(`Expected Wuzhang finale story to load the new sixty-sixth intro pages and backgrounds: ${JSON.stringify(wuzhangStoryState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-sixth-story.png', fullPage: true });
|
|
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const enteredSixtySixthBattle = await page.evaluate(() => {
|
|
const state = window.__HEROS_DEBUG__?.battle();
|
|
return state?.scene === 'BattleScene' && state?.battleId === 'sixty-sixth-battle-wuzhang-final';
|
|
});
|
|
if (enteredSixtySixthBattle) {
|
|
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 === 'sixty-sixth-battle-wuzhang-final' && state?.battleOutcome === null && state?.phase === 'idle';
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-sixty-sixth-battle.png', fullPage: true });
|
|
|
|
const sixtySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
|
const sixtySixthEnemies = sixtySixthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
|
const sixtySixthAllies = sixtySixthBattleState.units.filter((unit) => unit.faction === 'ally');
|
|
if (
|
|
sixtySixthBattleState.camera?.mapWidth !== 132 ||
|
|
sixtySixthBattleState.camera?.mapHeight !== 114 ||
|
|
sixtySixthBattleState.mapTextureKey !== 'battle-map-sixty-sixth' ||
|
|
sixtySixthBattleState.mapTextureReady !== true ||
|
|
sixtySixthBattleState.mapBackgroundReady !== true ||
|
|
sixtySixthBattleState.victoryConditionLabel !== '오장원 본영 방어와 장부 계승 확보' ||
|
|
!sixtySixthBattleState.objectives?.some((objective) => objective.id === 'hold-wuzhang-main-camp' && objective.label === '사마의 오장원 본대 격파') ||
|
|
!sixtySixthBattleState.objectives?.some((objective) => objective.id === 'stop-zhang-he-final-pursuit' && objective.label === '장합 최종 추격대 저지') ||
|
|
!sixtySixthBattleState.objectives?.some((objective) => objective.id === 'silence-guo-huai-wuzhang-beacons' && objective.label === '곽회 봉화 차단') ||
|
|
!sixtySixthBattleState.objectives?.some((objective) => objective.id === 'preserve-wuzhang-camps' && objective.label === '오장원 본영 확보') ||
|
|
!sixtySixthBattleState.objectives?.some((objective) => objective.id === 'protect-returning-villages' && objective.label === '귀환로 마을 보호') ||
|
|
sixtySixthEnemies.length < 40 ||
|
|
!sixtySixthEnemies.some((unit) => unit.id === 'northern-twelfth-leader-sima-yi' && unit.combatPortraitKey === 'portrait-sima-yi' && unit.textureBase === 'unit-sima-yi') ||
|
|
!sixtySixthEnemies.some((unit) => unit.id === 'northern-twelfth-officer-zhang-he') ||
|
|
!sixtySixthEnemies.some((unit) => unit.id === 'northern-twelfth-officer-guo-huai') ||
|
|
!wuzhangPriorityUnits.every((unitId) => sixtySixthAllies.some((unit) => unit.id === unitId)) ||
|
|
!unitUsesTexture(sixtySixthBattleState, 'jiang-wei', 'unit-jiang-wei') ||
|
|
!unitUsesTexture(sixtySixthBattleState, 'ma-dai', 'unit-ma-dai') ||
|
|
!unitUsesTexture(sixtySixthBattleState, 'wang-ping', 'unit-wang-ping') ||
|
|
!unitUsesTexture(sixtySixthBattleState, 'huang-quan', 'unit-huang-quan') ||
|
|
!unitUsesTexture(sixtySixthBattleState, 'li-yan', 'unit-li-yan') ||
|
|
sixtySixthAllies.some((unit) => unit.id === 'liu-bei')
|
|
) {
|
|
throw new Error(`Expected Wuzhang finale battle to use the new map, Sima Yi/Zhang He/Guo Huai objectives, and inherited northern campaign officers: ${JSON.stringify(sixtySixthBattleState)}`);
|
|
}
|
|
|
|
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 sixtySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
sixtySixthCampState?.campBattleId !== 'sixty-sixth-battle-wuzhang-final' ||
|
|
sixtySixthCampState.campaign?.step !== 'sixty-sixth-camp' ||
|
|
sixtySixthCampState.campTitle !== '오장원 최종전 후 군영' ||
|
|
sixtySixthCampState.availableDialogueIds?.length !== 2 ||
|
|
!sixtySixthCampState.availableDialogueIds.every((id) => id.startsWith('northern-twelfth-')) ||
|
|
sixtySixthCampState.availableVisitIds?.length !== 1 ||
|
|
!sixtySixthCampState.availableVisitIds.every((id) => id.startsWith('northern-twelfth-')) ||
|
|
sixtySixthCampState.rosterCollection?.total < 22 ||
|
|
!sixtySixthCampState.report?.bonds?.some((bond) => bond.id === 'zhuge-liang__jiang-wei_northern-twelfth')
|
|
) {
|
|
throw new Error(`Expected Wuzhang finale aftermath camp to expose twelfth northern campaign events and bonds: ${JSON.stringify(sixtySixthCampState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-sixty-sixth-camp.png', fullPage: true });
|
|
|
|
await page.mouse.click(966, 38);
|
|
await page.waitForTimeout(180);
|
|
const postSixtySixthProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (
|
|
postSixtySixthProgressState.campaignProgress?.completedKnown !== 66 ||
|
|
postSixtySixthProgressState.campaignProgress?.totalKnown !== 66 ||
|
|
postSixtySixthProgressState.campaignProgress?.activeChapter?.id !== 'northern-campaign' ||
|
|
postSixtySixthProgressState.campaignProgress?.latestBattleTitle !== '오장원 최종전' ||
|
|
postSixtySixthProgressState.campaignProgress?.nextBattleTitle !== '북벌의 끝과 남은 뜻'
|
|
) {
|
|
throw new Error(`Expected post-sixty-sixth progress tab to complete Wuzhang finale and expose the final epilogue: ${JSON.stringify(postSixtySixthProgressState?.campaignProgress)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-post-sixty-sixth-progress.png', fullPage: true });
|
|
|
|
await page.mouse.click(1160, 38);
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('StoryScene');
|
|
});
|
|
await page.waitForFunction(() => {
|
|
const story = window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.();
|
|
return story?.currentPageId === 'sixty-sixth-victory-wuzhang-held' && story?.backgroundReady === true;
|
|
});
|
|
await page.screenshot({ path: 'dist/verification-ending-epilogue-story.png', fullPage: true });
|
|
await page.evaluate(async () => {
|
|
const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
for (let i = 0; i < 20; i += 1) {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
if (activeScenes.includes('EndingScene')) {
|
|
break;
|
|
}
|
|
window.__HEROS_DEBUG__?.scene('StoryScene')?.advance?.();
|
|
await delay(520);
|
|
}
|
|
});
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
const ending = window.__HEROS_DEBUG__?.scene('EndingScene')?.getDebugState?.();
|
|
return activeScenes.includes('EndingScene') && ending?.ready === true && ending?.campaignStep === 'ending-complete';
|
|
});
|
|
const endingState = await page.evaluate(() => ({
|
|
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
|
|
ending: window.__HEROS_DEBUG__?.scene('EndingScene')?.getDebugState?.() ?? null
|
|
}));
|
|
if (
|
|
!endingState.activeScenes.includes('EndingScene') ||
|
|
endingState.ending?.campaignStep !== 'ending-complete' ||
|
|
endingState.ending?.latestBattleId !== 'sixty-sixth-battle-wuzhang-final' ||
|
|
endingState.ending?.completedBattles !== 66 ||
|
|
endingState.ending?.endingTitle !== '북벌의 끝과 남은 뜻'
|
|
) {
|
|
throw new Error(`Expected final epilogue to mark ending-complete and show EndingScene: ${JSON.stringify(endingState)}`);
|
|
}
|
|
await page.screenshot({ path: 'dist/verification-ending-screen.png', fullPage: true });
|
|
|
|
await page.evaluate(() => {
|
|
const game = window.__HEROS_GAME__;
|
|
game?.scene.stop('CampScene');
|
|
game?.scene.stop('BattleScene');
|
|
game?.scene.stop('StoryScene');
|
|
game?.scene.stop('EndingScene');
|
|
game?.scene.start('TitleScene');
|
|
});
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('TitleScene') && !activeScenes.includes('CampScene') && !activeScenes.includes('EndingScene');
|
|
});
|
|
await page.waitForTimeout(800);
|
|
for (const [x, y] of [
|
|
[962, 310],
|
|
[962, 306],
|
|
[962, 314]
|
|
]) {
|
|
await page.mouse.click(x, y);
|
|
await page.waitForTimeout(300);
|
|
const continuedToEnding = await page.evaluate(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
return activeScenes.includes('EndingScene');
|
|
});
|
|
if (continuedToEnding) {
|
|
break;
|
|
}
|
|
}
|
|
for (let attempt = 0; attempt < 24; attempt += 1) {
|
|
const continuedToEnding = await page.evaluate(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
const ending = window.__HEROS_DEBUG__?.scene('EndingScene')?.getDebugState?.();
|
|
return activeScenes.includes('EndingScene') && ending?.campaignStep === 'ending-complete';
|
|
});
|
|
if (continuedToEnding) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(320);
|
|
}
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
const ending = window.__HEROS_DEBUG__?.scene('EndingScene')?.getDebugState?.();
|
|
return activeScenes.includes('EndingScene') && ending?.campaignStep === 'ending-complete';
|
|
});
|
|
const titleContinueState = await page.evaluate(() => ({
|
|
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
|
|
ending: window.__HEROS_DEBUG__?.scene('EndingScene')?.getDebugState?.() ?? null
|
|
}));
|
|
if (!titleContinueState.activeScenes.includes('EndingScene') || titleContinueState.ending?.campaignStep !== 'ending-complete') {
|
|
throw new Error(`Expected title continue to reopen the ending screen: ${JSON.stringify(titleContinueState)}`);
|
|
}
|
|
|
|
console.log(`Verified title-to-ending flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, final capture, northern campaign preparation, Qishan road first northern expedition, Tianshui advance, Jieting crisis, Jiang Wei joining, Qishan retreat, Chencang siege, Wudu/Yinping commanderies, Hanzhong rainy-pass defense, Qishan renewed offensive, Lucheng pursuit, Weishui camps, Weishui north bank pressure, final epilogue, ending-complete save state, and title continue to ending screen 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;
|
|
}
|
|
}
|
|
|
|
async function waitForStoryReady(page) {
|
|
await page.waitForFunction(() => {
|
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
|
const storyState = window.__HEROS_GAME__?.scene.getScene('StoryScene')?.getDebugState?.();
|
|
return (
|
|
activeScenes.includes('StoryScene') &&
|
|
storyState?.ready === true &&
|
|
storyState?.backgroundReady === true &&
|
|
storyState?.activeTexture === storyState?.background
|
|
);
|
|
}, undefined, { timeout: 90000 });
|
|
}
|
|
|
|
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 && !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.reserveTrainingLine?.includes('대기 훈련') ||
|
|
!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' ||
|
|
typeof state.sortiePlan.reserveTrainingExpPreview !== 'number' ||
|
|
typeof state.sortiePlan.reserveTrainingEquipmentPreview !== 'number' ||
|
|
typeof state.sortiePlan.reserveTrainingBondPreview !== 'number'
|
|
) {
|
|
throw new Error(`Expected sortie preparation to expose deployment plan, reserve training, bond count, and terrain score: ${JSON.stringify(state.sortiePlan)}`);
|
|
}
|
|
|
|
if (
|
|
!state.sortieFocusedUnit ||
|
|
!state.sortieFocusedUnit.id ||
|
|
!state.sortieFocusedUnit.classRole ||
|
|
!state.sortieFocusedUnit.statLine?.includes('무 ') ||
|
|
!state.sortieFocusedUnit.equipmentLine?.includes('Lv') ||
|
|
!state.sortieFocusedUnit.bondLine?.includes('공명') ||
|
|
!state.sortieFocusedUnit.terrainLine?.includes('지형') ||
|
|
typeof state.sortieFocusedUnit.terrainScore !== 'number' ||
|
|
!state.sortieFocusedUnit.terrainGrade ||
|
|
!Array.isArray(state.sortieFocusedUnit.equipment) ||
|
|
state.sortieFocusedUnit.equipment.length !== 3 ||
|
|
state.sortieFocusedUnit.equipment.some((item) => !item.label || !item.itemName || typeof item.next !== 'number' || !item.bonusText)
|
|
) {
|
|
throw new Error(`Expected sortie preparation to expose focused officer analysis: ${JSON.stringify(state.sortieFocusedUnit)}`);
|
|
}
|
|
|
|
if (
|
|
!state.reserveTrainingFocus ||
|
|
!Array.isArray(state.reserveTrainingFocusOptions) ||
|
|
state.reserveTrainingFocusOptions.length !== 3 ||
|
|
!state.reserveTrainingFocusOptions.some((focus) => focus.id === 'bond-practice' && focus.bondExpGained > 0)
|
|
) {
|
|
throw new Error(`Expected sortie preparation to expose selectable reserve drill focuses: ${JSON.stringify(state.reserveTrainingFocusOptions)}`);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async function clickReserveTrainingFocus(page, focusId) {
|
|
const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
|
if (!state?.sortieVisible || !Array.isArray(state.reserveTrainingFocusOptions)) {
|
|
throw new Error(`Cannot click reserve drill ${focusId}; sortie reserve drill selector is not visible: ${JSON.stringify(state)}`);
|
|
}
|
|
|
|
const index = state.reserveTrainingFocusOptions.findIndex((focus) => focus.id === focusId);
|
|
if (index < 0) {
|
|
throw new Error(`Cannot click reserve drill ${focusId}; focus is missing: ${JSON.stringify(state.reserveTrainingFocusOptions)}`);
|
|
}
|
|
|
|
const selectorX = 574;
|
|
const buttonWidth = Math.floor((314 - 40) / state.reserveTrainingFocusOptions.length);
|
|
await page.mouse.click(selectorX + 12 + index * (buttonWidth + 8) + buttonWidth / 2, 655);
|
|
await page.waitForTimeout(120);
|
|
}
|
|
|