feat: finish FHD battle sidebar

This commit is contained in:
2026-07-12 13:33:04 +09:00
parent cb66deef9d
commit 31d76c6252
2 changed files with 539 additions and 208 deletions

View File

@@ -24,14 +24,14 @@ async function moveLegacyUi(page, x, y, options) {
async function clickBattleDeploymentStart(page) {
const point = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const layout = scene?.layout;
const startButton = window.__HEROS_DEBUG__?.battle()?.battleHud?.deploymentPanel?.startButtonBounds;
const canvas = document.querySelector('canvas');
const bounds = canvas?.getBoundingClientRect();
if (!scene || !layout || !canvas || !bounds) {
if (!scene || !startButton || !canvas || !bounds) {
return null;
}
const logicalX = layout.panelX + 24 + (layout.panelWidth - 48) / 2;
const logicalY = layout.panelY + layout.panelHeight - 76 + 17;
const logicalX = startButton.x + startButton.width / 2;
const logicalY = startButton.y + startButton.height / 2;
return {
x: bounds.left + logicalX * bounds.width / scene.scale.width,
y: bounds.top + logicalY * bounds.height / scene.scale.height
@@ -174,8 +174,10 @@ try {
boundsInside(firstBattleRecentActions, firstBattleProbe.panelBounds) &&
boundsInside(firstBattleRecentActions, firstBattleProbe.sceneBounds) &&
firstBattleRecentActions?.compact === false &&
firstBattleRecentActions.height === 118 &&
firstBattleRecentActions.height === 126 &&
firstBattleRecentActions.rowCount === Math.min(3, firstBattleProbe.battleLog.length) &&
firstBattleRecentActions.textBounds.length === firstBattleRecentActions.rowCount &&
firstBattleRecentActions.textBounds.every((bounds) => boundsInside(bounds, firstBattleRecentActions)) &&
firstBattleProbe.sideTexts.includes('최근 행동') &&
firstBattleRecentActions.bottom === firstBattleRecentActions.y + firstBattleRecentActions.height &&
firstBattleRecentActions.gapToMiniMap >= 6 &&
@@ -5470,12 +5472,33 @@ async function assertLargeRosterHud(browser, url) {
JSON.stringify([...battle.deployedAllyIds].sort()) === JSON.stringify([...expectedIds].sort())
);
}, expectedRosterIds, { timeout: 90000 });
const deploymentProbe = await readLargeRosterHudProbe(page);
assertLateBattleHeaderLayout(deploymentProbe, 'deployment');
assertLateBattleSidebarLayout(deploymentProbe, 'deployment', false);
assertLateBattleDeploymentLayout(deploymentProbe);
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-deployment.png`, fullPage: true });
await assertCanvasPainted(page, 'final battle deployment');
await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
if (scene?.phase === 'deployment') {
scene.confirmPreBattleDeployment?.();
}
scene?.hideBattleEventBanner?.();
scene?.renderSituationPanel?.();
});
await page.waitForFunction(() => {
const hud = window.__HEROS_DEBUG__?.battle()?.battleHud;
return hud?.miniMap?.visible === true && hud?.sidebar?.bounds && hud?.deploymentPanel === null;
}, undefined, { timeout: 30000 });
const situationProbe = await readLargeRosterHudProbe(page);
assertLateBattleHeaderLayout(situationProbe, 'situation');
assertLateBattleSidebarLayout(situationProbe, 'situation', true);
await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
scene?.renderRosterPanel?.('ally', '출전 위치 확정. 행동할 장수를 선택하세요.');
});
await page.waitForFunction(() => {
@@ -5519,6 +5542,25 @@ async function assertLargeRosterHud(browser, url) {
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-roster-last-page.png`, fullPage: true });
await assertCanvasPainted(page, 'final battle roster last page');
await page.evaluate(() => {
window.__HEROS_GAME__?.scene.getScene('BattleScene')?.renderRosterPanel?.('enemy', '적군 전력을 확인하세요.');
});
await page.waitForFunction(() => {
const roster = window.__HEROS_DEBUG__?.battle()?.battleHud?.rosterPanel;
return roster?.tab === 'enemy' && roster?.pageCount > 1;
}, undefined, { timeout: 30000 });
await page.evaluate(() => new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(resolve));
}));
const enemyPage = await readLargeRosterHudProbe(page);
assert(
enemyPage.roster.rowLayouts.some((row) => row.unitName.length >= 8),
`Expected the enemy roster fixture to exercise a long unit name: ${JSON.stringify(enemyPage)}`
);
assertLargeRosterPageLayout(enemyPage, 'enemy first');
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-enemy-roster.png`, fullPage: true });
await assertCanvasPainted(page, 'final battle enemy roster');
await page.evaluate((selectedSortieUnitIds) => {
window.__HEROS_GAME__?.scene.getScene('BattleScene')?.scene.restart({
battleId: 'sixty-sixth-battle-wuzhang-final',
@@ -5559,6 +5601,12 @@ async function readLargeRosterHudProbe(page) {
roster: hud?.rosterPanel ?? null,
recentActions: hud?.recentActions ?? null,
miniMap: hud?.miniMap ?? null,
header: hud?.header ?? null,
sidebar: hud?.sidebar ?? null,
deploymentPanel: hud?.deploymentPanel ?? null,
sceneBounds: scene
? { x: 0, y: 0, width: scene.scale.width, height: scene.scale.height }
: null,
panelBounds: scene
? { x: scene.layout.panelX, y: scene.layout.panelY, width: scene.layout.panelWidth, height: scene.layout.panelHeight }
: null
@@ -5566,11 +5614,113 @@ async function readLargeRosterHudProbe(page) {
});
}
function assertLateBattleHeaderLayout(probe, label) {
const { header, panelBounds, sceneBounds } = probe;
const headerBounds = [
header?.titleBounds,
header?.turnBounds,
header?.speedBounds,
header?.objectiveBounds,
header?.defeatBounds,
header?.sortieOrderBounds
];
assert(
isFiniteBounds(panelBounds) &&
isFiniteBounds(sceneBounds) &&
boundsInside(panelBounds, sceneBounds) &&
headerBounds.every((bounds) => boundsInside(bounds, panelBounds) && boundsInside(bounds, sceneBounds)),
`Expected every late-battle ${label} header element inside the FHD side panel and scene: ${JSON.stringify(probe)}`
);
assert(
header.titleBounds.y + header.titleBounds.height <= Math.min(header.turnBounds.y, header.speedBounds.y) &&
header.turnBounds.x + header.turnBounds.width <= header.speedBounds.x &&
Math.max(
header.turnBounds.y + header.turnBounds.height,
header.speedBounds.y + header.speedBounds.height
) <= header.objectiveBounds.y &&
header.objectiveBounds.y + header.objectiveBounds.height <= header.defeatBounds.y &&
header.defeatBounds.y + header.defeatBounds.height <= header.sortieOrderBounds.y &&
header.sortieOrderBounds.y + header.sortieOrderBounds.height <= header.contentTop,
`Expected the wrapped late-battle ${label} objective, defeat condition, and sortie order to remain separated: ${JSON.stringify(header)}`
);
}
function assertLateBattleSidebarLayout(probe, label, expectMiniMap) {
const { sidebar, miniMap, header, panelBounds, sceneBounds } = probe;
assert(
isFiniteBounds(sidebar?.bounds) &&
boundsInside(sidebar.bounds, panelBounds) &&
boundsInside(sidebar.bounds, sceneBounds) &&
sidebar.contentTop === header?.contentTop &&
sidebar.bounds.y >= sidebar.contentTop &&
sidebar.bounds.y + sidebar.bounds.height <= sidebar.contentBottom,
`Expected the late-battle ${label} sidebar inside its FHD content region: ${JSON.stringify(probe)}`
);
assert(
header.sortieOrderBounds.y + header.sortieOrderBounds.height <= sidebar.bounds.y,
`Expected the late-battle ${label} sidebar below the sortie-order header: ${JSON.stringify(probe)}`
);
if (!expectMiniMap) {
assert(miniMap?.visible === false, `Expected deployment to reserve the full sidebar by hiding the minimap: ${JSON.stringify(probe)}`);
return;
}
assert(
miniMap?.visible === true &&
boundsInside(miniMap.frameBounds, panelBounds) &&
boundsInside(miniMap.frameBounds, sceneBounds) &&
sidebar.bounds.y + sidebar.bounds.height <= miniMap.frameBounds.y,
`Expected the late-battle ${label} sidebar to stay above the FHD minimap: ${JSON.stringify(probe)}`
);
}
function assertLateBattleDeploymentLayout(probe) {
const { deploymentPanel, sidebar, panelBounds, sceneBounds } = probe;
const roleBounds = deploymentPanel?.roleBounds ?? [];
const deploymentBounds = [
deploymentPanel?.headerBounds,
deploymentPanel?.noticeBounds,
...roleBounds,
deploymentPanel?.restoreButtonBounds,
deploymentPanel?.startButtonBounds
];
assert(
roleBounds.length > 0 && deploymentBounds.every((bounds) =>
boundsInside(bounds, sidebar?.bounds) &&
boundsInside(bounds, panelBounds) &&
boundsInside(bounds, sceneBounds)
) && boundsInside(deploymentPanel?.noticeTextBounds, deploymentPanel?.noticeBounds),
`Expected every late-battle deployment section inside the FHD sidebar and scene: ${JSON.stringify(probe)}`
);
assert(
deploymentPanel.headerBounds.y + deploymentPanel.headerBounds.height <= deploymentPanel.noticeBounds.y &&
deploymentPanel.noticeBounds.y + deploymentPanel.noticeBounds.height <= roleBounds[0].y &&
roleBounds.every((bounds, index) =>
index === 0 || roleBounds[index - 1].y + roleBounds[index - 1].height <= bounds.y
) &&
roleBounds.at(-1).y + roleBounds.at(-1).height <= deploymentPanel.restoreButtonBounds.y &&
deploymentPanel.restoreButtonBounds.y + deploymentPanel.restoreButtonBounds.height <= deploymentPanel.startButtonBounds.y,
`Expected the late-battle deployment header, notice, roles, and actions not to overlap: ${JSON.stringify(deploymentPanel)}`
);
}
function assertLargeRosterPageLayout(probe, label) {
const { roster, panelBounds } = probe;
assert(boundsInside(roster.listBounds, panelBounds), `Expected roster rows on page ${label} inside the side panel: ${JSON.stringify(probe)}`);
assert(boundsInside(roster.navigationBounds, panelBounds), `Expected roster navigation on page ${label} inside the side panel: ${JSON.stringify(probe)}`);
assert(boundsInside(roster.messageBounds, panelBounds), `Expected roster guidance on page ${label} inside the side panel: ${JSON.stringify(probe)}`);
assert(
roster.rowLayouts.length === roster.visibleUnitIds.length &&
roster.rowLayouts.every((row) =>
boundsInside(row.rowBounds, roster.listBounds) &&
boundsInside(row.nameBounds, row.rowBounds) &&
boundsInside(row.metaBounds, row.rowBounds) &&
boundsInside(row.hpBounds, row.rowBounds) &&
boundsInside(row.gaugeBounds, row.rowBounds) &&
row.nameBounds.x + row.nameBounds.width <= row.hpBounds.x &&
row.metaBounds.x + row.metaBounds.width <= row.gaugeBounds.x
),
`Expected roster page ${label} names, classes, HP, and gauges to remain in separate columns: ${JSON.stringify(probe)}`
);
assert(
roster.listBounds.y + roster.listBounds.height <= roster.navigationBounds.y &&
roster.navigationBounds.y + roster.navigationBounds.height <= roster.messageBounds.y &&