feat: adopt 1920x1080 baseline

This commit is contained in:
2026-07-12 11:45:01 +09:00
parent c8e8ee4b3a
commit 3daad2861b
14 changed files with 1231 additions and 729 deletions

View File

@@ -2,8 +2,36 @@
import { readFileSync } from 'node:fs';
import { chromium } from 'playwright';
const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/';
const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/');
const rewardDataFiles = ['src/game/data/battles.ts', 'src/game/scenes/CampScene.ts'];
const baselineViewport = { width: 1920, height: 1080 };
const legacyUiScale = 1.5;
const legacyUiClickPoints = new Set([
'962,240',
'962,306',
'962,310',
'962,314',
'738,642',
'1120,38',
'966,38',
'724,38',
'1068,646',
'974,436',
'1160,38',
'576,38',
'180,315',
'798,38',
'650,38',
'974,482',
'1120,245',
'702,386'
]);
function withCanvasRenderer(url) {
const parsed = new URL(url);
parsed.searchParams.set('renderer', 'canvas');
return parsed.toString();
}
function assertNoLegacyNumericRewardLabels() {
const legacyNumericRewardPattern = /itemRewards:\s*\[[^\]\r\n]*(["'])(쌀|금|군주)\s+\d+\1/u;
@@ -30,6 +58,164 @@ function unitUsesTexture(state, unitId, textureBase) {
return state?.units?.some((unit) => unit.id === unitId && unit.textureBase === textureBase && unit.actionTexture === `${textureBase}-actions`);
}
async function clickLegacyUi(page, x, y, options) {
await page.mouse.click(x * legacyUiScale, y * legacyUiScale, options);
}
async function moveLegacyUi(page, x, y, options) {
await page.mouse.move(x * legacyUiScale, y * legacyUiScale, options);
}
async function clickBattleDeploymentStart(page) {
const point = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const layout = scene?.layout;
const canvas = document.querySelector('canvas');
const bounds = canvas?.getBoundingClientRect();
if (!scene || !layout || !canvas || !bounds) {
return null;
}
const logicalX = layout.panelX + 24 + (layout.panelWidth - 48) / 2;
const logicalY = layout.panelY + layout.panelHeight - 76 + 17;
return {
x: bounds.left + logicalX * bounds.width / scene.scale.width,
y: bounds.top + logicalY * bounds.height / scene.scale.height
};
});
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`Expected a runtime deployment start point: ${JSON.stringify(point)}`);
}
await page.mouse.click(point.x, point.y);
}
async function openBattleMapMenu(page) {
const point = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const layout = scene?.layout;
const canvas = document.querySelector('canvas');
const bounds = canvas?.getBoundingClientRect();
if (!scene || !layout || !canvas || !bounds) {
return null;
}
const visibleColumns = Math.floor(layout.gridWidth / layout.tileSize);
const visibleRows = Math.floor(layout.gridHeight / layout.tileSize);
for (let row = 0; row < visibleRows; row += 1) {
for (let column = 0; column < visibleColumns; column += 1) {
const tileX = scene.cameraTileX + column;
const tileY = scene.cameraTileY + row;
if (!scene.isInBounds(tileX, tileY) || scene.isOccupied(tileX, tileY)) {
continue;
}
const logicalX = layout.gridX + (column + 0.5) * layout.tileSize;
const logicalY = layout.gridY + (row + 0.5) * layout.tileSize;
return {
x: bounds.left + logicalX * bounds.width / scene.scale.width,
y: bounds.top + logicalY * bounds.height / scene.scale.height
};
}
}
return null;
});
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`Expected a visible empty battle tile for the map menu: ${JSON.stringify(point)}`);
}
await page.mouse.click(point.x, point.y, { button: 'right' });
await page.waitForFunction(() => Boolean(window.__HEROS_GAME__?.scene.getScene('BattleScene')?.mapMenuLayout));
}
async function clickBattleMapMenuAction(page, action) {
const point = await page.evaluate((requestedAction) => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const layout = scene?.mapMenuLayout;
const canvas = document.querySelector('canvas');
const bounds = canvas?.getBoundingClientRect();
const actionIndex = layout?.actions.indexOf(requestedAction) ?? -1;
if (!scene || !layout || !canvas || !bounds || actionIndex < 0) {
return null;
}
const logicalX = layout.left + layout.width / 2;
const logicalY = layout.top + layout.padding + (actionIndex + 0.5) * layout.buttonHeight;
return {
x: bounds.left + logicalX * bounds.width / scene.scale.width,
y: bounds.top + logicalY * bounds.height / scene.scale.height
};
}, action);
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`Expected a runtime battle map-menu point for ${action}: ${JSON.stringify(point)}`);
}
await page.mouse.click(point.x, point.y);
}
async function clickBattleSaveSlot(page, slot) {
const point = await page.evaluate((requestedSlot) => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const canvas = document.querySelector('canvas');
const bounds = canvas?.getBoundingClientRect();
if (!scene || !canvas || !bounds || !scene.saveSlotPanelMode) {
return null;
}
const rows = scene.saveSlotPanelObjects
.filter((object) => object.type === 'Rectangle' && object.input && object.displayWidth > 200 && object.displayHeight < 100)
.sort((left, right) => left.y - right.y);
const row = rows[requestedSlot - 1];
const rowBounds = row?.getBounds();
if (!rowBounds) {
return null;
}
const logicalX = rowBounds.centerX;
const logicalY = rowBounds.centerY;
return {
x: bounds.left + logicalX * bounds.width / scene.scale.width,
y: bounds.top + logicalY * bounds.height / scene.scale.height
};
}, slot);
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`Expected a runtime battle save-slot row for slot ${slot}: ${JSON.stringify(point)}`);
}
await page.mouse.click(point.x, point.y);
}
async function moveBattlePointerToRightEdge(page) {
const point = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const layout = scene?.layout;
const canvas = document.querySelector('canvas');
const bounds = canvas?.getBoundingClientRect();
if (!scene || !layout || !canvas || !bounds) {
return null;
}
const logicalX = layout.gridX + layout.gridWidth - 1;
const logicalY = layout.gridY + layout.gridHeight / 2;
return {
x: bounds.left + logicalX * bounds.width / scene.scale.width,
y: bounds.top + logicalY * bounds.height / scene.scale.height
};
});
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`Expected the runtime battle-grid right edge: ${JSON.stringify(point)}`);
}
await page.mouse.move(point.x, point.y);
}
function installLegacyUiClickScaling(page) {
const click = page.mouse.click.bind(page.mouse);
page.mouse.click = (x, y, options) => {
// Only authored 1280x720 controls are scaled; runtime map/minimap coordinates pass through unchanged.
const scale = legacyUiClickPoints.has(`${x},${y}`) ? legacyUiScale : 1;
return click(x * scale, y * scale, options);
};
}
let serverProcess;
let browser;
@@ -39,7 +225,8 @@ try {
serverProcess = await ensureLocalServer(targetUrl);
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const page = await browser.newPage({ viewport: baselineViewport });
installLegacyUiClickScaling(page);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => window.localStorage.clear());
@@ -110,7 +297,7 @@ try {
};
});
if (result.canvasWidth !== 1280 || result.canvasHeight !== 720) {
if (result.canvasWidth !== 1920 || result.canvasHeight !== 1080) {
throw new Error(`Unexpected canvas size: ${result.canvasWidth}x${result.canvasHeight}`);
}
@@ -226,9 +413,8 @@ try {
});
await page.waitForTimeout(700);
await page.mouse.click(260, 300, { button: 'right' });
await page.waitForTimeout(120);
await page.mouse.click(182, 196);
await openBattleMapMenu(page);
await clickBattleMapMenuAction(page, 'threat');
await page.waitForTimeout(160);
const threatState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
@@ -339,11 +525,10 @@ try {
scene.hideBattleEventBanner();
});
await page.mouse.click(260, 300, { button: 'right' });
await page.waitForTimeout(120);
await page.mouse.click(182, 212);
await page.waitForTimeout(120);
await page.mouse.click(400, 213);
await openBattleMapMenu(page);
await clickBattleMapMenuAction(page, 'save');
await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.saveSlotPanelMode === 'save');
await clickBattleSaveSlot(page, 1);
await page.waitForFunction(() => (
window.localStorage.getItem('heros-web:battle:first-battle-zhuo-commandery:slot-1') !== null ||
window.__HEROS_DEBUG__?.battle()?.turnPromptMode === 'save-overwrite-confirm'
@@ -362,11 +547,10 @@ try {
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, 246);
await page.waitForTimeout(120);
await page.mouse.click(400, 213);
await openBattleMapMenu(page);
await clickBattleMapMenuAction(page, 'load');
await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.saveSlotPanelMode === 'load');
await clickBattleSaveSlot(page, 1);
await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.turnPromptMode === 'load-confirm');
await page.keyboard.press('Enter');
await page.waitForFunction(() => {
@@ -383,7 +567,7 @@ try {
throw new Error(`Expected a scrollable battle map: ${JSON.stringify(cameraBeforeScroll)}`);
}
await page.mouse.move(872, 360);
await moveBattlePointerToRightEdge(page);
await page.waitForTimeout(520);
const cameraAfterEdgeScroll = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.camera);
if (!cameraAfterEdgeScroll || cameraAfterEdgeScroll.x <= cameraBeforeScroll.x) {
@@ -9645,7 +9829,7 @@ async function startDeploymentIfNeeded(page, battleId) {
throw new Error(`Expected ${battleId} deployment to contain allied officers: ${JSON.stringify(state)}`);
}
await page.mouse.click(1085, 637);
await clickBattleDeploymentStart(page);
await page.waitForFunction((expectedBattleId) => {
const battle = window.__HEROS_DEBUG__?.battle();
return battle?.battleId === expectedBattleId && battle?.phase === 'idle' && battle?.triggeredBattleEvents?.includes('opening');
@@ -10026,7 +10210,7 @@ async function clickSortieRosterUnit(page, unitId) {
while (index < scroll || index >= scroll + maxVisibleRows) {
const direction = index < scroll ? -1 : 1;
const pointerRow = Math.max(0, Math.min(maxVisibleRows - 1, index - scroll));
await page.mouse.move(rosterX, rosterY + pointerRow * rowGap);
await moveLegacyUi(page, rosterX, rosterY + pointerRow * rowGap);
await page.mouse.wheel(0, direction * 360);
await page.waitForTimeout(80);
state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
@@ -10037,7 +10221,7 @@ async function clickSortieRosterUnit(page, unitId) {
scroll = nextScroll;
}
await page.mouse.click(rosterX, rosterY + (index - scroll) * rowGap);
await clickLegacyUi(page, rosterX, rosterY + (index - scroll) * rowGap);
await page.waitForTimeout(120);
}
@@ -10057,6 +10241,6 @@ async function clickReserveTrainingFocus(page, focusId) {
const selectorX = staged ? 448 : 574;
const selectorWidth = staged ? 438 : 314;
const buttonWidth = Math.floor((selectorWidth - 40) / state.reserveTrainingFocusOptions.length);
await page.mouse.click(selectorX + 12 + index * (buttonWidth + 8) + buttonWidth / 2, staged ? 548 : 655);
await clickLegacyUi(page, selectorX + 12 + index * (buttonWidth + 8) + buttonWidth / 2, staged ? 548 : 655);
await page.waitForTimeout(120);
}