feat: optimize battle ui for hd displays
This commit is contained in:
@@ -390,7 +390,21 @@ try {
|
|||||||
throw new Error(`Expected edge scrolling to move camera right: ${JSON.stringify({ cameraBeforeScroll, cameraAfterEdgeScroll })}`);
|
throw new Error(`Expected edge scrolling to move camera right: ${JSON.stringify({ cameraBeforeScroll, cameraAfterEdgeScroll })}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.mouse.click(1138, 553);
|
const miniMapClickPoint = await page.evaluate(() => {
|
||||||
|
const miniMap = window.__HEROS_DEBUG__?.battle()?.battleHud?.miniMap;
|
||||||
|
if (!miniMap?.visible || !miniMap.mapBounds || !Number.isFinite(miniMap.cellSize)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: miniMap.mapBounds.x + miniMap.mapBounds.width - miniMap.cellSize / 2,
|
||||||
|
y: miniMap.mapBounds.y + miniMap.cellSize / 2,
|
||||||
|
miniMap
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (!miniMapClickPoint) {
|
||||||
|
throw new Error('Expected the tactical minimap bounds before testing camera navigation.');
|
||||||
|
}
|
||||||
|
await page.mouse.click(miniMapClickPoint.x, miniMapClickPoint.y);
|
||||||
await page.waitForTimeout(160);
|
await page.waitForTimeout(160);
|
||||||
const cameraAfterMiniMapClick = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.camera);
|
const cameraAfterMiniMapClick = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.camera);
|
||||||
if (
|
if (
|
||||||
@@ -398,7 +412,7 @@ try {
|
|||||||
cameraAfterMiniMapClick.x < cameraAfterMiniMapClick.mapWidth - cameraAfterMiniMapClick.visibleColumns ||
|
cameraAfterMiniMapClick.x < cameraAfterMiniMapClick.mapWidth - cameraAfterMiniMapClick.visibleColumns ||
|
||||||
cameraAfterMiniMapClick.y !== 0
|
cameraAfterMiniMapClick.y !== 0
|
||||||
) {
|
) {
|
||||||
throw new Error(`Expected minimap click to move camera to the northeast: ${JSON.stringify(cameraAfterMiniMapClick)}`);
|
throw new Error(`Expected minimap click to move camera to the northeast: ${JSON.stringify({ miniMapClickPoint, cameraAfterMiniMapClick })}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const readyUnits = result.battleState.units.filter((unit) => !unit.acted);
|
const readyUnits = result.battleState.units.filter((unit) => !unit.acted);
|
||||||
@@ -9931,7 +9945,7 @@ async function ensureSortiePrepStep(page, expectedStep) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function launchSortieFromCurrentStep(page) {
|
async function launchSortieFromCurrentStep(page) {
|
||||||
const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
let state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
if (!state?.sortieVisible) {
|
if (!state?.sortieVisible) {
|
||||||
throw new Error(`Cannot launch sortie; preparation is not visible: ${JSON.stringify(state)}`);
|
throw new Error(`Cannot launch sortie; preparation is not visible: ${JSON.stringify(state)}`);
|
||||||
}
|
}
|
||||||
@@ -9941,13 +9955,35 @@ async function launchSortieFromCurrentStep(page) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.sortieOperationOrder?.required && !state.sortieOperationOrder.complete) {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__HEROS_GAME__?.scene.getScene('CampScene')?.selectSortieOrder?.('elite');
|
||||||
|
});
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const camp = window.__HEROS_DEBUG__?.camp();
|
||||||
|
return camp?.sortieVisible === true && camp?.sortieOperationOrder?.selectedId === 'elite' && camp?.sortieOperationOrder?.complete === true;
|
||||||
|
}, undefined, { timeout: 30000 });
|
||||||
|
state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
}
|
||||||
|
|
||||||
await ensureSortiePrepStep(page, 'loadout');
|
await ensureSortiePrepStep(page, 'loadout');
|
||||||
|
await page.waitForTimeout(160);
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await page.waitForFunction(() => {
|
try {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
await page.waitForFunction(() => {
|
||||||
const camp = window.__HEROS_DEBUG__?.camp();
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
return !activeScenes.includes('CampScene') || camp?.sortieVisible !== true;
|
const camp = window.__HEROS_DEBUG__?.camp();
|
||||||
}, undefined, { timeout: 30000 });
|
return !activeScenes.includes('CampScene') || camp?.sortieVisible !== true;
|
||||||
|
}, undefined, { timeout: 30000 });
|
||||||
|
} catch (error) {
|
||||||
|
const stalledState = await page.evaluate(() => ({
|
||||||
|
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
|
||||||
|
camp: window.__HEROS_DEBUG__?.camp() ?? null,
|
||||||
|
battle: window.__HEROS_DEBUG__?.battle() ?? null,
|
||||||
|
gameScenes: window.__HEROS_GAME__?.scene.getScenes(true).map((scene) => scene.scene.key) ?? []
|
||||||
|
}));
|
||||||
|
throw new Error(`Sortie launch stalled after Enter: ${JSON.stringify(stalledState)}\n${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickSortieRosterUnit(page, unitId) {
|
async function clickSortieRosterUnit(page, unitId) {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ try {
|
|||||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
await waitForTitle(page);
|
await waitForTitle(page);
|
||||||
await assertReleaseShellReady(page);
|
await assertReleaseShellReady(page);
|
||||||
|
await assertHdPlusCanvasFit(page);
|
||||||
|
await assertHdPlusInputMapping(browser, targetUrl);
|
||||||
|
await assertLargeRosterHud(browser, targetUrl);
|
||||||
await page.screenshot({ path: `${screenshotDir}/rc-title-first-run.png`, fullPage: true });
|
await page.screenshot({ path: `${screenshotDir}/rc-title-first-run.png`, fullPage: true });
|
||||||
await assertCanvasPainted(page, 'title first run');
|
await assertCanvasPainted(page, 'title first run');
|
||||||
|
|
||||||
@@ -60,16 +63,36 @@ try {
|
|||||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
const state = window.__HEROS_DEBUG__?.battle();
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
const sideTexts = textValues(scene?.sidePanelObjects);
|
const sideTexts = textValues(scene?.sidePanelObjects);
|
||||||
|
const miniMapTexts = textValues((scene?.miniMapObjects ?? []).filter((object) => object?.active && object?.visible));
|
||||||
|
const miniMapFrame = (scene?.miniMapObjects ?? []).find((object) => object?.name === 'mini-map-frame');
|
||||||
return {
|
return {
|
||||||
battleId: state?.battleId,
|
battleId: state?.battleId,
|
||||||
objectiveText: scene?.objectiveTrackerText?.text,
|
objectiveText: scene?.objectiveTrackerText?.text,
|
||||||
objectiveSubText: scene?.objectiveTrackerSubText?.text,
|
objectiveSubText: scene?.objectiveTrackerSubText?.text,
|
||||||
sideTexts
|
sideTexts,
|
||||||
|
miniMapTexts,
|
||||||
|
actualMiniMapFrameBounds: miniMapFrame ? boundsValue(miniMapFrame.getBounds()) : null,
|
||||||
|
hud: state?.battleHud ?? null,
|
||||||
|
camera: state?.camera ?? null,
|
||||||
|
battleLog: [...(state?.battleLog ?? [])],
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
function textValues(objects = []) {
|
function textValues(objects = []) {
|
||||||
return objects.filter((object) => object?.type === 'Text').map((object) => object.text);
|
return objects.filter((object) => object?.type === 'Text').map((object) => object.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function boundsValue(bounds) {
|
||||||
|
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
assert(firstBattleProbe.battleId === 'first-battle-zhuo-commandery', `Expected first battle: ${JSON.stringify(firstBattleProbe)}`);
|
assert(firstBattleProbe.battleId === 'first-battle-zhuo-commandery', `Expected first battle: ${JSON.stringify(firstBattleProbe)}`);
|
||||||
assert(firstBattleProbe.objectiveText?.startsWith('승리 목표:'), `Expected clear victory objective label: ${JSON.stringify(firstBattleProbe)}`);
|
assert(firstBattleProbe.objectiveText?.startsWith('승리 목표:'), `Expected clear victory objective label: ${JSON.stringify(firstBattleProbe)}`);
|
||||||
@@ -85,6 +108,50 @@ try {
|
|||||||
!firstBattleProbe.sideTexts.some((text) => text.includes('...')),
|
!firstBattleProbe.sideTexts.some((text) => text.includes('...')),
|
||||||
`Expected sortie doctrine opening message without ASCII truncation: ${JSON.stringify(firstBattleProbe.sideTexts)}`
|
`Expected sortie doctrine opening message without ASCII truncation: ${JSON.stringify(firstBattleProbe.sideTexts)}`
|
||||||
);
|
);
|
||||||
|
const firstBattleMiniMap = firstBattleProbe.hud?.miniMap;
|
||||||
|
const firstBattleRecentActions = firstBattleProbe.hud?.recentActions;
|
||||||
|
assert(
|
||||||
|
firstBattleMiniMap?.visible === true &&
|
||||||
|
firstBattleMiniMap.title === '전장 지도' &&
|
||||||
|
sameJsonValue(firstBattleMiniMap.legend, ['아', '적', '목']) &&
|
||||||
|
firstBattleProbe.sceneBounds?.width === 1280 &&
|
||||||
|
firstBattleProbe.sceneBounds?.height === 720 &&
|
||||||
|
firstBattleMiniMap.cellSize >= 1 &&
|
||||||
|
firstBattleMiniMap.objectiveHighlighted === true &&
|
||||||
|
firstBattleMiniMap.selectedUnitId === null &&
|
||||||
|
firstBattleMiniMap.selectedHighlighted === false &&
|
||||||
|
firstBattleProbe.miniMapTexts.includes('전장 지도') &&
|
||||||
|
['아', '적', '목'].every((label) => firstBattleProbe.miniMapTexts.includes(label)) &&
|
||||||
|
sameJsonValue(firstBattleProbe.actualMiniMapFrameBounds, firstBattleMiniMap.frameBounds) &&
|
||||||
|
isFiniteBounds(firstBattleProbe.sceneBounds) &&
|
||||||
|
isFiniteBounds(firstBattleProbe.panelBounds) &&
|
||||||
|
isFiniteBounds(firstBattleMiniMap.frameBounds) &&
|
||||||
|
isFiniteBounds(firstBattleMiniMap.headerBounds) &&
|
||||||
|
isFiniteBounds(firstBattleMiniMap.mapBounds) &&
|
||||||
|
isFiniteBounds(firstBattleMiniMap.viewportBounds) &&
|
||||||
|
boundsInside(firstBattleProbe.panelBounds, firstBattleProbe.sceneBounds) &&
|
||||||
|
boundsInside(firstBattleMiniMap.frameBounds, firstBattleProbe.panelBounds) &&
|
||||||
|
boundsInside(firstBattleMiniMap.headerBounds, firstBattleMiniMap.frameBounds) &&
|
||||||
|
boundsInside(firstBattleMiniMap.mapBounds, firstBattleMiniMap.frameBounds) &&
|
||||||
|
boundsInside(firstBattleMiniMap.viewportBounds, firstBattleMiniMap.mapBounds, 2) &&
|
||||||
|
firstBattleMiniMap.mapBounds.width === firstBattleProbe.camera?.mapWidth * firstBattleMiniMap.cellSize &&
|
||||||
|
firstBattleMiniMap.mapBounds.height === firstBattleProbe.camera?.mapHeight * firstBattleMiniMap.cellSize &&
|
||||||
|
firstBattleMiniMap.headerBounds.y + firstBattleMiniMap.headerBounds.height <= firstBattleMiniMap.mapBounds.y,
|
||||||
|
`Expected a framed tactical minimap with title, legend, objective emphasis, and separate map hit bounds: ${JSON.stringify(firstBattleProbe)}`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
isFiniteBounds(firstBattleRecentActions) &&
|
||||||
|
boundsInside(firstBattleRecentActions, firstBattleProbe.panelBounds) &&
|
||||||
|
boundsInside(firstBattleRecentActions, firstBattleProbe.sceneBounds) &&
|
||||||
|
firstBattleRecentActions?.compact === true &&
|
||||||
|
firstBattleRecentActions.height === 84 &&
|
||||||
|
firstBattleRecentActions.rowCount === Math.min(2, firstBattleProbe.battleLog.length) &&
|
||||||
|
firstBattleProbe.sideTexts.includes('최근 행동') &&
|
||||||
|
firstBattleRecentActions.bottom === firstBattleRecentActions.y + firstBattleRecentActions.height &&
|
||||||
|
firstBattleRecentActions.gapToMiniMap >= 6 &&
|
||||||
|
firstBattleRecentActions.bottom <= firstBattleMiniMap.frameBounds.y,
|
||||||
|
`Expected the 720p recent-action panel to compact to two rows without touching the minimap frame: ${JSON.stringify(firstBattleProbe.hud)}`
|
||||||
|
);
|
||||||
await setDebugQueryParam(page, 'debugForceBondChain', '1');
|
await setDebugQueryParam(page, 'debugForceBondChain', '1');
|
||||||
const firstBattleBondChain = await page.evaluate(() => {
|
const firstBattleBondChain = await page.evaluate(() => {
|
||||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
@@ -3997,6 +4064,264 @@ async function assertReleaseShellReady(page) {
|
|||||||
assert(state.gameApiPresent === true, `Expected release QA game handle to be enabled: ${JSON.stringify(state)}`);
|
assert(state.gameApiPresent === true, `Expected release QA game handle to be enabled: ${JSON.stringify(state)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assertHdPlusCanvasFit(page) {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
viewport: { width: 1600, height: 900 },
|
||||||
|
expectedBounds: { x: 0, y: 0, width: 1600, height: 900 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewport: { width: 1920, height: 1200 },
|
||||||
|
expectedBounds: { x: 0, y: 60, width: 1920, height: 1080 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
await page.setViewportSize(testCase.viewport);
|
||||||
|
await page.waitForFunction(({ expectedBounds }) => {
|
||||||
|
const canvas = document.querySelector('canvas');
|
||||||
|
if (!canvas) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bounds = canvas.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
canvas.width === 1280 &&
|
||||||
|
canvas.height === 720 &&
|
||||||
|
Math.abs(bounds.x - expectedBounds.x) <= 1 &&
|
||||||
|
Math.abs(bounds.y - expectedBounds.y) <= 1 &&
|
||||||
|
Math.abs(bounds.width - expectedBounds.width) <= 1 &&
|
||||||
|
Math.abs(bounds.height - expectedBounds.height) <= 1
|
||||||
|
);
|
||||||
|
}, testCase, { timeout: 30000 });
|
||||||
|
|
||||||
|
const state = await page.evaluate(() => {
|
||||||
|
const canvas = document.querySelector('canvas');
|
||||||
|
const bounds = canvas?.getBoundingClientRect();
|
||||||
|
const game = window.__HEROS_GAME__;
|
||||||
|
return {
|
||||||
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
||||||
|
gameSize: game ? { width: game.scale.width, height: game.scale.height } : null,
|
||||||
|
canvas: canvas && bounds
|
||||||
|
? {
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert(state.canvas?.width === 1280 && state.canvas?.height === 720, `Expected a fixed HD game surface: ${JSON.stringify(state)}`);
|
||||||
|
assert(state.gameSize?.width === 1280 && state.gameSize?.height === 720, `Expected fixed HD scene coordinates: ${JSON.stringify(state)}`);
|
||||||
|
assert(
|
||||||
|
boundsNear(state.canvas?.bounds, testCase.expectedBounds),
|
||||||
|
`Expected HD surface to fit ${testCase.viewport.width}x${testCase.viewport.height}: ${JSON.stringify(state)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const canvas = document.querySelector('canvas');
|
||||||
|
const bounds = canvas?.getBoundingClientRect();
|
||||||
|
return Boolean(
|
||||||
|
canvas &&
|
||||||
|
bounds &&
|
||||||
|
canvas.width === 1280 &&
|
||||||
|
canvas.height === 720 &&
|
||||||
|
Math.abs(bounds.x) <= 1 &&
|
||||||
|
Math.abs(bounds.y) <= 1 &&
|
||||||
|
Math.abs(bounds.width - 1280) <= 1 &&
|
||||||
|
Math.abs(bounds.height - 720) <= 1
|
||||||
|
);
|
||||||
|
}, undefined, { timeout: 30000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundsNear(actual, expected, tolerance = 1) {
|
||||||
|
return Boolean(
|
||||||
|
actual &&
|
||||||
|
Math.abs(actual.x - expected.x) <= tolerance &&
|
||||||
|
Math.abs(actual.y - expected.y) <= tolerance &&
|
||||||
|
Math.abs(actual.width - expected.width) <= tolerance &&
|
||||||
|
Math.abs(actual.height - expected.height) <= tolerance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertHdPlusInputMapping(browser, url) {
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
||||||
|
try {
|
||||||
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.evaluate(() => window.localStorage.clear());
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
await waitForTitle(page);
|
||||||
|
|
||||||
|
const clickPoint = await page.evaluate(() => {
|
||||||
|
const canvas = document.querySelector('canvas');
|
||||||
|
const bounds = canvas?.getBoundingClientRect();
|
||||||
|
if (!canvas || !bounds) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: bounds.x + (962 / canvas.width) * bounds.width,
|
||||||
|
y: bounds.y + (240 / canvas.height) * bounds.height,
|
||||||
|
canvasBounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert(
|
||||||
|
clickPoint && boundsNear(clickPoint.canvasBounds, { x: 0, y: 60, width: 1920, height: 1080 }),
|
||||||
|
`Expected a centered HD canvas before testing scaled input: ${JSON.stringify(clickPoint)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.mouse.click(clickPoint.x, clickPoint.y);
|
||||||
|
await waitForStoryReady(page);
|
||||||
|
const activeScenes = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []);
|
||||||
|
assert(activeScenes.includes('StoryScene'), `Expected scaled title input to enter the story: ${JSON.stringify(activeScenes)}`);
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertLargeRosterHud(browser, url) {
|
||||||
|
const expectedRosterIds = [
|
||||||
|
'zhuge-liang',
|
||||||
|
'jiang-wei',
|
||||||
|
'wang-ping',
|
||||||
|
'zhao-yun',
|
||||||
|
'ma-dai',
|
||||||
|
'ma-chao',
|
||||||
|
'huang-quan',
|
||||||
|
'li-yan'
|
||||||
|
];
|
||||||
|
const battleUrl = new URL(url);
|
||||||
|
battleUrl.searchParams.set('debugBattle', 'sixty-sixth-battle-wuzhang-final');
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
try {
|
||||||
|
await page.goto(battleUrl.toString(), { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const battle = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return battle?.battleId === 'sixty-sixth-battle-wuzhang-final' && battle?.mapBackgroundReady === true;
|
||||||
|
}, undefined, { timeout: 90000 });
|
||||||
|
await page.evaluate((selectedSortieUnitIds) => {
|
||||||
|
window.__HEROS_GAME__?.scene.getScene('BattleScene')?.scene.restart({
|
||||||
|
battleId: 'sixty-sixth-battle-wuzhang-final',
|
||||||
|
selectedSortieUnitIds
|
||||||
|
});
|
||||||
|
}, expectedRosterIds);
|
||||||
|
await page.waitForFunction((expectedIds) => {
|
||||||
|
const battle = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return (
|
||||||
|
battle?.battleId === 'sixty-sixth-battle-wuzhang-final' &&
|
||||||
|
battle?.mapBackgroundReady === true &&
|
||||||
|
JSON.stringify([...battle.deployedAllyIds].sort()) === JSON.stringify([...expectedIds].sort())
|
||||||
|
);
|
||||||
|
}, expectedRosterIds, { timeout: 90000 });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
if (scene?.phase === 'deployment') {
|
||||||
|
scene.confirmPreBattleDeployment?.();
|
||||||
|
}
|
||||||
|
scene?.hideBattleEventBanner?.();
|
||||||
|
scene?.renderRosterPanel?.('ally', '출전 위치 확정. 행동할 장수를 선택하세요.');
|
||||||
|
});
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const roster = window.__HEROS_DEBUG__?.battle()?.battleHud?.rosterPanel;
|
||||||
|
return roster?.tab === 'ally' && roster?.pageCount > 1;
|
||||||
|
}, undefined, { timeout: 30000 });
|
||||||
|
|
||||||
|
const firstPage = await readLargeRosterHudProbe(page);
|
||||||
|
assert(firstPage.roster.totalCount === expectedRosterIds.length, `Expected the exact eight-officer sortie roster: ${JSON.stringify(firstPage)}`);
|
||||||
|
assert(firstPage.roster.page === 0 && firstPage.roster.pageCount >= 2, `Expected paged late-battle roster: ${JSON.stringify(firstPage)}`);
|
||||||
|
assert(
|
||||||
|
firstPage.roster.visibleUnitIds.length > 0 && firstPage.roster.visibleUnitIds.length <= firstPage.roster.rowsPerPage,
|
||||||
|
`Expected only the current roster page to render: ${JSON.stringify(firstPage)}`
|
||||||
|
);
|
||||||
|
assertLargeRosterPageLayout(firstPage, 'first');
|
||||||
|
assert(firstPage.recentActions === null, `Expected the paged roster to prioritize selectable units over the action log: ${JSON.stringify(firstPage)}`);
|
||||||
|
|
||||||
|
const visibleRosterIds = new Set(firstPage.roster.visibleUnitIds);
|
||||||
|
let visibleRosterCount = firstPage.roster.visibleUnitIds.length;
|
||||||
|
let currentPage = firstPage;
|
||||||
|
for (let pageIndex = 1; pageIndex < firstPage.roster.pageCount; pageIndex += 1) {
|
||||||
|
await page.mouse.click(
|
||||||
|
currentPage.roster.navigationBounds.x + currentPage.roster.navigationBounds.width - 39,
|
||||||
|
currentPage.roster.navigationBounds.y + 15
|
||||||
|
);
|
||||||
|
await page.waitForFunction(
|
||||||
|
(expectedPage) => window.__HEROS_DEBUG__?.battle()?.battleHud?.rosterPanel?.page === expectedPage,
|
||||||
|
pageIndex,
|
||||||
|
{ timeout: 30000 }
|
||||||
|
);
|
||||||
|
currentPage = await readLargeRosterHudProbe(page);
|
||||||
|
visibleRosterCount += currentPage.roster.visibleUnitIds.length;
|
||||||
|
currentPage.roster.visibleUnitIds.forEach((unitId) => visibleRosterIds.add(unitId));
|
||||||
|
assertLargeRosterPageLayout(currentPage, `${pageIndex + 1}`);
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
visibleRosterCount === expectedRosterIds.length &&
|
||||||
|
JSON.stringify([...visibleRosterIds].sort()) === JSON.stringify([...expectedRosterIds].sort()),
|
||||||
|
`Expected roster paging to expose every selected officer exactly once: ${JSON.stringify({ expectedRosterIds, visibleRosterCount, visibleRosterIds: [...visibleRosterIds] })}`
|
||||||
|
);
|
||||||
|
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((selectedSortieUnitIds) => {
|
||||||
|
window.__HEROS_GAME__?.scene.getScene('BattleScene')?.scene.restart({
|
||||||
|
battleId: 'sixty-sixth-battle-wuzhang-final',
|
||||||
|
selectedSortieUnitIds
|
||||||
|
});
|
||||||
|
}, expectedRosterIds);
|
||||||
|
await page.waitForFunction((expectedIds) => {
|
||||||
|
const battle = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return (
|
||||||
|
battle?.battleId === 'sixty-sixth-battle-wuzhang-final' &&
|
||||||
|
battle?.mapBackgroundReady === true &&
|
||||||
|
battle?.phase === 'deployment' &&
|
||||||
|
JSON.stringify([...battle.deployedAllyIds].sort()) === JSON.stringify([...expectedIds].sort())
|
||||||
|
);
|
||||||
|
}, expectedRosterIds, { timeout: 90000 });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
scene?.confirmPreBattleDeployment?.();
|
||||||
|
scene?.hideBattleEventBanner?.();
|
||||||
|
scene?.renderRosterPanel?.('ally', '출전 위치 확정. 행동할 장수를 선택하세요.');
|
||||||
|
});
|
||||||
|
await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.battleHud?.rosterPanel?.pageCount > 1, undefined, { timeout: 30000 });
|
||||||
|
const restartedPage = await readLargeRosterHudProbe(page);
|
||||||
|
assert(
|
||||||
|
restartedPage.roster.page === 0,
|
||||||
|
`Expected a restarted battle to reset the roster to page one: ${JSON.stringify(restartedPage)}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLargeRosterHudProbe(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
const hud = window.__HEROS_DEBUG__?.battle()?.battleHud;
|
||||||
|
return {
|
||||||
|
roster: hud?.rosterPanel ?? null,
|
||||||
|
recentActions: hud?.recentActions ?? null,
|
||||||
|
miniMap: hud?.miniMap ?? null,
|
||||||
|
panelBounds: scene
|
||||||
|
? { x: scene.layout.panelX, y: scene.layout.panelY, width: scene.layout.panelWidth, height: scene.layout.panelHeight }
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.listBounds.y + roster.listBounds.height <= roster.navigationBounds.y &&
|
||||||
|
roster.navigationBounds.y + roster.navigationBounds.height <= roster.messageBounds.y &&
|
||||||
|
roster.gapToMiniMap >= 6,
|
||||||
|
`Expected roster page ${label} content to stay vertically separated: ${JSON.stringify(probe)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForStoryReady(page) {
|
async function waitForStoryReady(page) {
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
|
|||||||
@@ -1131,6 +1131,32 @@ type MiniMapLayout = {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
cellSize: number;
|
cellSize: number;
|
||||||
|
frameX: number;
|
||||||
|
frameY: number;
|
||||||
|
frameWidth: number;
|
||||||
|
frameHeight: number;
|
||||||
|
headerHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RecentActionLogLayout = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
compact: boolean;
|
||||||
|
rowCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RosterPanelLayout = {
|
||||||
|
tab: RosterTab;
|
||||||
|
page: number;
|
||||||
|
pageCount: number;
|
||||||
|
rowsPerPage: number;
|
||||||
|
totalCount: number;
|
||||||
|
visibleUnitIds: string[];
|
||||||
|
listBounds: { x: number; y: number; width: number; height: number };
|
||||||
|
navigationBounds: { x: number; y: number; width: number; height: number } | null;
|
||||||
|
messageBounds: { x: number; y: number; width: number; height: number } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UnitView = {
|
type UnitView = {
|
||||||
@@ -3336,6 +3362,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private markers: Phaser.GameObjects.Rectangle[] = [];
|
private markers: Phaser.GameObjects.Rectangle[] = [];
|
||||||
private objectiveMarkerObjects: Phaser.GameObjects.GameObject[] = [];
|
private objectiveMarkerObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private rosterTab: RosterTab = 'ally';
|
private rosterTab: RosterTab = 'ally';
|
||||||
|
private rosterPage = 0;
|
||||||
|
private rosterPanelLayout?: RosterPanelLayout;
|
||||||
private sidePanelObjects: Phaser.GameObjects.GameObject[] = [];
|
private sidePanelObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private commandButtons: CommandButton[] = [];
|
private commandButtons: CommandButton[] = [];
|
||||||
private commandMenuObjects: Phaser.GameObjects.GameObject[] = [];
|
private commandMenuObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
@@ -3424,6 +3452,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private miniMapUnitDots = new Map<string, Phaser.GameObjects.Rectangle>();
|
private miniMapUnitDots = new Map<string, Phaser.GameObjects.Rectangle>();
|
||||||
private miniMapViewport?: Phaser.GameObjects.Rectangle;
|
private miniMapViewport?: Phaser.GameObjects.Rectangle;
|
||||||
private miniMapVisible = true;
|
private miniMapVisible = true;
|
||||||
|
private recentActionLogLayout?: RecentActionLogLayout;
|
||||||
private cameraTileX = 0;
|
private cameraTileX = 0;
|
||||||
private cameraTileY = 0;
|
private cameraTileY = 0;
|
||||||
private edgeScrollElapsed = 0;
|
private edgeScrollElapsed = 0;
|
||||||
@@ -3445,6 +3474,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
init(data?: BattleSceneData) {
|
init(data?: BattleSceneData) {
|
||||||
configureBattleScenario(getBattleScenario(data?.battleId));
|
configureBattleScenario(getBattleScenario(data?.battleId));
|
||||||
|
this.rosterTab = 'ally';
|
||||||
|
this.rosterPage = 0;
|
||||||
|
this.rosterPanelLayout = undefined;
|
||||||
const campaign = getCampaignState();
|
const campaign = getCampaignState();
|
||||||
const selectedOrderId = campaign.sortieOrderSelection?.battleId === battleScenario.id
|
const selectedOrderId = campaign.sortieOrderSelection?.battleId === battleScenario.id
|
||||||
? campaign.sortieOrderSelection.orderId
|
? campaign.sortieOrderSelection.orderId
|
||||||
@@ -5531,7 +5563,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
||||||
const left = panelX + 24;
|
const left = panelX + 24;
|
||||||
const miniMapTop = this.miniMapLayout?.y ?? panelY + panelHeight - 156;
|
const miniMapTop = this.miniMapLayout?.frameY ?? panelY + panelHeight - 156;
|
||||||
const top = Math.max(panelY + 212, miniMapTop - 148);
|
const top = Math.max(panelY + 212, miniMapTop - 148);
|
||||||
const width = panelWidth - 48;
|
const width = panelWidth - 48;
|
||||||
const height = 128;
|
const height = 128;
|
||||||
@@ -5589,20 +5621,76 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private drawMiniMap() {
|
private drawMiniMap() {
|
||||||
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
||||||
const maxWidth = panelWidth - 48;
|
const maxWidth = panelWidth - 48;
|
||||||
const maxHeight = 132;
|
const maxHeight = 108;
|
||||||
const cellSize = Math.floor(Math.min(maxWidth / battleMap.width, maxHeight / battleMap.height));
|
const cellSize = Math.max(1, Math.floor(Math.min(maxWidth / battleMap.width, maxHeight / battleMap.height)));
|
||||||
const width = battleMap.width * cellSize;
|
const width = battleMap.width * cellSize;
|
||||||
const height = battleMap.height * cellSize;
|
const height = battleMap.height * cellSize;
|
||||||
const x = panelX + Math.floor((panelWidth - width) / 2);
|
const x = panelX + Math.floor((panelWidth - width) / 2);
|
||||||
const y = panelY + panelHeight - height - 24;
|
const y = panelY + panelHeight - height - 12;
|
||||||
this.miniMapLayout = { x, y, width, height, cellSize };
|
const headerHeight = 26;
|
||||||
|
const frameWidth = Math.min(maxWidth, Math.max(width + 12, 190));
|
||||||
|
const frameHeight = headerHeight + height + 6;
|
||||||
|
const frameX = panelX + Math.floor((panelWidth - frameWidth) / 2);
|
||||||
|
const frameY = y - headerHeight;
|
||||||
|
this.miniMapLayout = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
cellSize,
|
||||||
|
frameX,
|
||||||
|
frameY,
|
||||||
|
frameWidth,
|
||||||
|
frameHeight,
|
||||||
|
headerHeight
|
||||||
|
};
|
||||||
|
|
||||||
const frame = this.add.rectangle(x - 5, y - 5, width + 10, height + 10, 0x0a1014, 0.92);
|
const frame = this.add.rectangle(frameX, frameY, frameWidth, frameHeight, 0x0a1014, 0.96);
|
||||||
frame.setOrigin(0);
|
frame.setOrigin(0);
|
||||||
frame.setStrokeStyle(2, palette.gold, 0.72);
|
frame.setStrokeStyle(1, palette.gold, 0.78);
|
||||||
frame.setDepth(24);
|
frame.setDepth(24);
|
||||||
|
frame.setName('mini-map-frame');
|
||||||
this.miniMapObjects.push(frame);
|
this.miniMapObjects.push(frame);
|
||||||
|
|
||||||
|
const title = this.add.text(frameX + 10, frameY + 6, '전장 지도', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#f2e3bf',
|
||||||
|
fontStyle: '700'
|
||||||
|
});
|
||||||
|
title.setDepth(27);
|
||||||
|
title.setName('mini-map-title');
|
||||||
|
this.miniMapObjects.push(title);
|
||||||
|
|
||||||
|
const separator = this.add.rectangle(frameX + 5, frameY + headerHeight - 1, frameWidth - 10, 1, palette.gold, 0.34);
|
||||||
|
separator.setOrigin(0);
|
||||||
|
separator.setDepth(25);
|
||||||
|
this.miniMapObjects.push(separator);
|
||||||
|
|
||||||
|
const legendStartX = frameX + frameWidth - 82;
|
||||||
|
[
|
||||||
|
{ label: '아', tone: 0x4aa9ff, target: false },
|
||||||
|
{ label: '적', tone: 0xff715f, target: false },
|
||||||
|
{ label: '목', tone: palette.gold, target: true }
|
||||||
|
].forEach((legend, index) => {
|
||||||
|
const legendX = legendStartX + index * 27;
|
||||||
|
const dot = this.add.rectangle(legendX, frameY + 13, 7, 7, legend.target ? 0x0a1014 : legend.tone, 1);
|
||||||
|
if (legend.target) {
|
||||||
|
dot.setStrokeStyle(1, legend.tone, 1);
|
||||||
|
}
|
||||||
|
dot.setDepth(27);
|
||||||
|
this.miniMapObjects.push(dot);
|
||||||
|
|
||||||
|
const label = this.add.text(legendX + 6, frameY + 7, legend.label, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '9px',
|
||||||
|
color: legend.target ? '#ffdf7b' : '#c8d2dd',
|
||||||
|
fontStyle: '700'
|
||||||
|
});
|
||||||
|
label.setDepth(27);
|
||||||
|
this.miniMapObjects.push(label);
|
||||||
|
});
|
||||||
|
|
||||||
const hitArea = this.add.rectangle(x, y, width, height, 0x111820, 0.08);
|
const hitArea = this.add.rectangle(x, y, width, height, 0x111820, 0.08);
|
||||||
hitArea.setOrigin(0);
|
hitArea.setOrigin(0);
|
||||||
hitArea.setInteractive({ useHandCursor: true });
|
hitArea.setInteractive({ useHandCursor: true });
|
||||||
@@ -5790,9 +5878,21 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
if (!unit) {
|
if (!unit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const selected = this.selectedUnit?.id === unit.id;
|
||||||
|
const objective = unit.id === leaderUnitId;
|
||||||
|
const baseSize = Math.max(3, cellSize);
|
||||||
|
const markerSize = baseSize + (objective ? 2 : 0) + (selected ? 2 : 0);
|
||||||
dot.setPosition(x + unit.x * cellSize + cellSize / 2, y + unit.y * cellSize + cellSize / 2);
|
dot.setPosition(x + unit.x * cellSize + cellSize / 2, y + unit.y * cellSize + cellSize / 2);
|
||||||
|
dot.setDisplaySize(markerSize, markerSize);
|
||||||
dot.setVisible(unit.hp > 0);
|
dot.setVisible(unit.hp > 0);
|
||||||
dot.setFillStyle(unit.faction === 'ally' ? 0x4aa9ff : 0xff715f, unit.hp > 0 ? 0.96 : 0.18);
|
dot.setFillStyle(unit.faction === 'ally' ? 0x4aa9ff : 0xff715f, unit.hp > 0 ? 0.96 : 0.18);
|
||||||
|
if (selected) {
|
||||||
|
dot.setStrokeStyle(2, 0xffffff, 1);
|
||||||
|
} else if (objective) {
|
||||||
|
dot.setStrokeStyle(2, palette.gold, 1);
|
||||||
|
} else {
|
||||||
|
dot.setStrokeStyle(0, 0x000000, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.miniMapViewport?.setPosition(x + this.cameraTileX * cellSize, y + this.cameraTileY * cellSize);
|
this.miniMapViewport?.setPosition(x + this.cameraTileX * cellSize, y + this.cameraTileY * cellSize);
|
||||||
@@ -6369,6 +6469,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.pendingMove = undefined;
|
this.pendingMove = undefined;
|
||||||
this.phase = 'idle';
|
this.phase = 'idle';
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
|
this.updateMiniMap();
|
||||||
this.refreshUnitLegibilityStyles();
|
this.refreshUnitLegibilityStyles();
|
||||||
this.showEnemyUnitThreatRange(unit);
|
this.showEnemyUnitThreatRange(unit);
|
||||||
this.renderUnitDetail(unit, this.enemyDetailMessage(unit));
|
this.renderUnitDetail(unit, this.enemyDetailMessage(unit));
|
||||||
@@ -6385,6 +6486,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
this.phase = 'idle';
|
this.phase = 'idle';
|
||||||
|
this.updateMiniMap();
|
||||||
this.refreshUnitLegibilityStyles();
|
this.refreshUnitLegibilityStyles();
|
||||||
this.renderUnitDetail(unit, '이미 행동을 마친 장수입니다.');
|
this.renderUnitDetail(unit, '이미 행동을 마친 장수입니다.');
|
||||||
return;
|
return;
|
||||||
@@ -6396,6 +6498,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
this.showMoveRange(unit);
|
this.showMoveRange(unit);
|
||||||
|
this.updateMiniMap();
|
||||||
this.refreshUnitLegibilityStyles();
|
this.refreshUnitLegibilityStyles();
|
||||||
this.renderUnitDetail(unit, '이동할 파란 칸을 선택하세요.');
|
this.renderUnitDetail(unit, '이동할 파란 칸을 선택하세요.');
|
||||||
}
|
}
|
||||||
@@ -21077,37 +21180,52 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private sideContentBottom(padding = 16) {
|
private sideContentBottom(padding = 16) {
|
||||||
const panelBottom = this.layout.panelY + this.layout.panelHeight - padding;
|
const panelBottom = this.layout.panelY + this.layout.panelHeight - padding;
|
||||||
return this.miniMapVisible && this.miniMapLayout ? this.miniMapLayout.y - padding : panelBottom;
|
return this.miniMapVisible && this.miniMapLayout ? this.miniMapLayout.frameY - padding : panelBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
private recentActionLogPlacement(minTop: number) {
|
||||||
|
const bottom = this.sideContentBottom(6);
|
||||||
|
const availableHeight = bottom - minTop;
|
||||||
|
const height = availableHeight >= 118 ? 118 : availableHeight >= 84 ? 84 : 0;
|
||||||
|
return height > 0
|
||||||
|
? { y: bottom - height, height, compact: height < 100 }
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRecentActionLogPanel(x: number, y: number, width: number, height = 118) {
|
private renderRecentActionLogPanel(x: number, y: number, width: number, height = 118) {
|
||||||
|
const compact = height < 100;
|
||||||
|
const headerHeight = compact ? 28 : 32;
|
||||||
|
const rowHeight = compact ? 21 : 24;
|
||||||
|
const rowStep = compact ? 24 : 28;
|
||||||
|
const maxRows = compact ? 2 : 3;
|
||||||
const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x0b1118, 0.96));
|
const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x0b1118, 0.96));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setDepth(30);
|
bg.setDepth(30);
|
||||||
bg.setStrokeStyle(1, 0x647485, 0.68);
|
bg.setStrokeStyle(1, 0x647485, 0.68);
|
||||||
|
|
||||||
const title = this.trackSideObject(this.add.text(x + 12, y + 8, '최근 행동', {
|
const title = this.trackSideObject(this.add.text(x + 12, y + (compact ? 6 : 8), '최근 행동', {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '15px',
|
fontSize: compact ? '14px' : '15px',
|
||||||
color: '#f2e3bf',
|
color: '#f2e3bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
title.setDepth(31);
|
title.setDepth(31);
|
||||||
|
|
||||||
const countText = this.trackSideObject(this.add.text(x + width - 12, y + 9, `${this.battleLog.length}`, {
|
const countText = this.trackSideObject(this.add.text(x + width - 12, y + (compact ? 7 : 9), `${this.battleLog.length}`, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '12px',
|
fontSize: compact ? '11px' : '12px',
|
||||||
color: '#9fb0bf',
|
color: '#9fb0bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
countText.setOrigin(1, 0);
|
countText.setOrigin(1, 0);
|
||||||
countText.setDepth(31);
|
countText.setDepth(31);
|
||||||
|
|
||||||
const entries = this.battleLog.slice(0, 3);
|
const entries = this.battleLog.slice(0, maxRows);
|
||||||
|
this.recentActionLogLayout = { x, y, width, height, compact, rowCount: entries.length };
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
const emptyText = this.trackSideObject(this.add.text(x + 12, y + 45, '아직 기록된 행동이 없습니다.', {
|
const emptyText = this.trackSideObject(this.add.text(x + 12, y + (compact ? 37 : 45), '아직 기록된 행동이 없습니다.', {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '13px',
|
fontSize: compact ? '12px' : '13px',
|
||||||
color: '#9fb0bf'
|
color: '#9fb0bf'
|
||||||
}));
|
}));
|
||||||
emptyText.setDepth(31);
|
emptyText.setDepth(31);
|
||||||
@@ -21115,35 +21233,38 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entries.forEach((entry, index) => {
|
entries.forEach((entry, index) => {
|
||||||
const rowY = y + 32 + index * 28;
|
const rowY = y + headerHeight + index * rowStep;
|
||||||
const { turn, text } = this.battleLogDisplayParts(entry);
|
const { turn, text } = this.battleLogDisplayParts(entry);
|
||||||
const visual = this.battleLogVisual(entry);
|
const visual = this.battleLogVisual(entry);
|
||||||
const row = this.trackSideObject(this.add.rectangle(x + 8, rowY, width - 16, 24, index === 0 ? 0x17232e : 0x101820, index === 0 ? 0.98 : 0.88));
|
const row = this.trackSideObject(this.add.rectangle(x + 8, rowY, width - 16, rowHeight, index === 0 ? 0x17232e : 0x101820, index === 0 ? 0.98 : 0.88));
|
||||||
row.setOrigin(0);
|
row.setOrigin(0);
|
||||||
row.setDepth(31);
|
row.setDepth(31);
|
||||||
row.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.34);
|
row.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.34);
|
||||||
|
|
||||||
const iconFrame = this.trackSideObject(this.add.rectangle(x + 23, rowY + 12, 22, 22, 0x0a0f14, 0.96));
|
const iconX = x + (compact ? 21 : 23);
|
||||||
|
const iconY = rowY + rowHeight / 2;
|
||||||
|
const iconSize = compact ? 18 : 22;
|
||||||
|
const iconFrame = this.trackSideObject(this.add.rectangle(iconX, iconY, iconSize, iconSize, 0x0a0f14, 0.96));
|
||||||
iconFrame.setDepth(32);
|
iconFrame.setDepth(32);
|
||||||
iconFrame.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.52);
|
iconFrame.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.52);
|
||||||
const icon = this.trackSideIcon(x + 23, rowY + 12, visual.icon, 21);
|
const icon = this.trackSideIcon(iconX, iconY, visual.icon, compact ? 17 : 21);
|
||||||
icon.setDepth(33);
|
icon.setDepth(33);
|
||||||
|
|
||||||
const turnText = this.trackSideObject(this.add.text(x + width - 12, rowY + 6, turn, {
|
const turnText = this.trackSideObject(this.add.text(x + width - 12, rowY + (compact ? 4 : 6), turn, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '10px',
|
fontSize: compact ? '9px' : '10px',
|
||||||
color: '#9fb0bf',
|
color: '#9fb0bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
turnText.setOrigin(1, 0);
|
turnText.setOrigin(1, 0);
|
||||||
turnText.setDepth(32);
|
turnText.setDepth(32);
|
||||||
|
|
||||||
const eventText = this.trackSideObject(this.add.text(x + 42, rowY + 5, this.truncateBattleLogText(text, index === 0 ? 39 : 34), {
|
const eventText = this.trackSideObject(this.add.text(x + (compact ? 37 : 42), rowY + (compact ? 3 : 5), this.truncateBattleLogText(text, compact ? 32 : index === 0 ? 39 : 34), {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: index === 0 ? '13px' : '12px',
|
fontSize: compact ? (index === 0 ? '12px' : '11px') : index === 0 ? '13px' : '12px',
|
||||||
color: index === 0 ? '#e8dfca' : '#c8d2dd',
|
color: index === 0 ? '#e8dfca' : '#c8d2dd',
|
||||||
fontStyle: index === 0 ? '700' : '400',
|
fontStyle: index === 0 ? '700' : '400',
|
||||||
fixedWidth: width - 92
|
fixedWidth: width - (compact ? 84 : 92)
|
||||||
}));
|
}));
|
||||||
eventText.setDepth(32);
|
eventText.setDepth(32);
|
||||||
});
|
});
|
||||||
@@ -21218,8 +21339,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const allyCount = battleUnits.filter((unit) => unit.faction === 'ally').length;
|
const allyCount = battleUnits.filter((unit) => unit.faction === 'ally').length;
|
||||||
const enemyCount = battleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp > 0).length;
|
const enemyCount = battleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp > 0).length;
|
||||||
const objectiveStates = this.objectiveStates(this.battleOutcome);
|
const objectiveStates = this.objectiveStates(this.battleOutcome);
|
||||||
const logHeight = 118;
|
const logPlacement = this.recentActionLogPlacement(top + 232);
|
||||||
const logTop = Math.max(top + 236, this.sideContentBottom(10) - logHeight);
|
const logTop = logPlacement?.y ?? this.sideContentBottom(6);
|
||||||
const messageTop = top + 168;
|
const messageTop = top + 168;
|
||||||
const messageHeight = Math.max(52, Math.min(72, logTop - messageTop - 12));
|
const messageHeight = Math.max(52, Math.min(72, logTop - messageTop - 12));
|
||||||
|
|
||||||
@@ -21236,15 +21357,23 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
this.renderPanelMessage(this.compactSituationMessage(message), left, messageTop, width, messageHeight);
|
this.renderPanelMessage(this.compactSituationMessage(message), left, messageTop, width, messageHeight);
|
||||||
this.renderRecentActionLogPanel(left, logTop, width, logHeight);
|
if (logPlacement) {
|
||||||
|
this.renderRecentActionLogPanel(left, logPlacement.y, width, logPlacement.height);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const guideHeight = this.renderTacticalGuideCard(left, messageTop, width);
|
const compactGuide = logTop - messageTop < 178;
|
||||||
|
const guideHeight = this.renderTacticalGuideCard(left, messageTop, width, compactGuide);
|
||||||
const objectiveTop = messageTop + (guideHeight > 0 ? guideHeight + 12 : 0);
|
const objectiveTop = messageTop + (guideHeight > 0 ? guideHeight + 12 : 0);
|
||||||
|
const objectiveHeight = logTop - objectiveTop - 8;
|
||||||
|
|
||||||
this.renderObjectiveStateGroups(objectiveStates, left, objectiveTop, width, Math.max(72, logTop - objectiveTop - 8));
|
if (objectiveHeight >= 58) {
|
||||||
this.renderRecentActionLogPanel(left, logTop, width, logHeight);
|
this.renderObjectiveStateGroups(objectiveStates, left, objectiveTop, width, objectiveHeight);
|
||||||
|
}
|
||||||
|
if (logPlacement) {
|
||||||
|
this.renderRecentActionLogPanel(left, logPlacement.y, width, logPlacement.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private compactSituationMessage(message: string) {
|
private compactSituationMessage(message: string) {
|
||||||
@@ -21255,26 +21384,44 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTacticalGuideCard(x: number, y: number, width: number) {
|
private renderTacticalGuideCard(x: number, y: number, width: number, compact = false) {
|
||||||
const guide = battleScenario.tacticalGuide;
|
const guide = battleScenario.tacticalGuide;
|
||||||
if (!guide) {
|
if (!guide) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const height = 96;
|
const height = compact ? 56 : 96;
|
||||||
const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x101820, 0.92));
|
const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x101820, 0.92));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setDepth(30);
|
bg.setDepth(30);
|
||||||
bg.setStrokeStyle(1, palette.gold, 0.5);
|
bg.setStrokeStyle(1, palette.gold, 0.5);
|
||||||
|
|
||||||
const title = this.trackSideObject(this.add.text(x + 12, y + 8, '작전 흐름', {
|
const title = this.trackSideObject(this.add.text(x + 12, y + (compact ? 7 : 8), '작전 흐름', {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '15px',
|
fontSize: compact ? '14px' : '15px',
|
||||||
color: '#f2e3bf',
|
color: '#f2e3bf',
|
||||||
fontStyle: '700'
|
fontStyle: '700'
|
||||||
}));
|
}));
|
||||||
title.setDepth(31);
|
title.setDepth(31);
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
const summary = this.trackSideObject(this.add.text(
|
||||||
|
x + 12,
|
||||||
|
y + 30,
|
||||||
|
this.truncateBattleLogText(`진군 ${guide.route} · ${guide.focus}`, 42),
|
||||||
|
{
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#d4dce6',
|
||||||
|
fontStyle: '700',
|
||||||
|
fixedWidth: width - 24,
|
||||||
|
maxLines: 1
|
||||||
|
}
|
||||||
|
));
|
||||||
|
summary.setDepth(31);
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
const route = this.trackSideObject(this.add.text(x + 12, y + 31, this.truncateBattleLogText(`진군: ${guide.route}`, 34), {
|
const route = this.trackSideObject(this.add.text(x + 12, y + 31, this.truncateBattleLogText(`진군: ${guide.route}`, 34), {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
@@ -21619,7 +21766,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderRosterPanel(tab: RosterTab = this.rosterTab, message?: string) {
|
private renderRosterPanel(tab: RosterTab = this.rosterTab, message?: string) {
|
||||||
|
const tabChanged = this.rosterTab !== tab;
|
||||||
this.rosterTab = tab;
|
this.rosterTab = tab;
|
||||||
|
if (tabChanged) {
|
||||||
|
this.rosterPage = 0;
|
||||||
|
}
|
||||||
this.clearSidePanelContent();
|
this.clearSidePanelContent();
|
||||||
this.setMiniMapVisible(true);
|
this.setMiniMapVisible(true);
|
||||||
|
|
||||||
@@ -21650,23 +21801,126 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
const units = battleUnits.filter((unit) => unit.faction === tab);
|
const units = battleUnits.filter((unit) => unit.faction === tab);
|
||||||
const listTop = top + 54;
|
const listTop = top + 54;
|
||||||
units.forEach((unit, index) => {
|
const contentBottom = this.sideContentBottom();
|
||||||
this.renderRosterRow(unit, left, listTop + index * 50, width);
|
const fullListBottom = listTop + units.length * 50;
|
||||||
|
const needsPagination = fullListBottom + (message ? 64 : 0) > contentBottom;
|
||||||
|
let visibleUnits = units;
|
||||||
|
let rowsPerPage = Math.max(1, units.length);
|
||||||
|
let pageCount = 1;
|
||||||
|
let navigationBounds: RosterPanelLayout['navigationBounds'] = null;
|
||||||
|
let messageBounds: RosterPanelLayout['messageBounds'] = null;
|
||||||
|
let rowStep = 50;
|
||||||
|
|
||||||
|
if (needsPagination) {
|
||||||
|
const navigationHeight = 30;
|
||||||
|
const navigationGap = 8;
|
||||||
|
const messageReserve = message ? 60 : 0;
|
||||||
|
rowStep = 46;
|
||||||
|
rowsPerPage = Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor((contentBottom - listTop - navigationHeight - navigationGap - messageReserve) / rowStep)
|
||||||
|
);
|
||||||
|
pageCount = Math.max(1, Math.ceil(units.length / rowsPerPage));
|
||||||
|
this.rosterPage = Phaser.Math.Clamp(this.rosterPage, 0, pageCount - 1);
|
||||||
|
const pageStart = this.rosterPage * rowsPerPage;
|
||||||
|
visibleUnits = units.slice(pageStart, pageStart + rowsPerPage);
|
||||||
|
} else {
|
||||||
|
this.rosterPage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleUnits.forEach((unit, index) => {
|
||||||
|
this.renderRosterRow(unit, left, listTop + index * rowStep, width);
|
||||||
});
|
});
|
||||||
|
|
||||||
const logHeight = 118;
|
const listBottom = listTop + visibleUnits.length * rowStep;
|
||||||
const logTop = this.sideContentBottom(10) - logHeight;
|
if (pageCount > 1) {
|
||||||
const listBottom = listTop + units.length * 50;
|
const navigationY = listBottom + 4;
|
||||||
const canShowLog = listBottom + 12 < logTop;
|
navigationBounds = { x: left, y: navigationY, width, height: 30 };
|
||||||
if (message) {
|
this.renderRosterPageNavigation(left, navigationY, width, pageCount, message);
|
||||||
const messageHeight = canShowLog ? 70 : 104;
|
|
||||||
const messageBottomLimit = canShowLog ? logTop - 10 : panelY + panelHeight - 124;
|
if (message) {
|
||||||
const messageTop = Math.min(messageBottomLimit - messageHeight, listBottom + 14);
|
const messageY = navigationY + navigationBounds.height + 8;
|
||||||
this.renderPanelMessage(message, left, messageTop, width, messageHeight);
|
const messageHeight = Math.min(52, contentBottom - messageY);
|
||||||
}
|
if (messageHeight >= 44) {
|
||||||
if (canShowLog) {
|
this.renderPanelMessage(message, left, messageY, width, messageHeight);
|
||||||
this.renderRecentActionLogPanel(left, logTop, width, logHeight);
|
messageBounds = { x: left, y: messageY, width, height: messageHeight };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tailTop = listBottom + 12;
|
||||||
|
if (message) {
|
||||||
|
const logPlacement = this.recentActionLogPlacement(tailTop + 62);
|
||||||
|
const messageBottomLimit = logPlacement ? logPlacement.y - 10 : contentBottom;
|
||||||
|
const messageHeight = Math.min(70, messageBottomLimit - tailTop);
|
||||||
|
if (messageHeight >= 44) {
|
||||||
|
this.renderPanelMessage(message, left, tailTop, width, messageHeight);
|
||||||
|
messageBounds = { x: left, y: tailTop, width, height: messageHeight };
|
||||||
|
}
|
||||||
|
if (logPlacement) {
|
||||||
|
this.renderRecentActionLogPanel(left, logPlacement.y, width, logPlacement.height);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const logPlacement = this.recentActionLogPlacement(tailTop);
|
||||||
|
if (logPlacement) {
|
||||||
|
this.renderRecentActionLogPanel(left, logPlacement.y, width, logPlacement.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.rosterPanelLayout = {
|
||||||
|
tab,
|
||||||
|
page: this.rosterPage,
|
||||||
|
pageCount,
|
||||||
|
rowsPerPage,
|
||||||
|
totalCount: units.length,
|
||||||
|
visibleUnitIds: visibleUnits.map((unit) => unit.id),
|
||||||
|
listBounds: { x: left, y: listTop, width, height: visibleUnits.length * rowStep },
|
||||||
|
navigationBounds,
|
||||||
|
messageBounds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRosterPageNavigation(x: number, y: number, width: number, pageCount: number, message?: string) {
|
||||||
|
const background = this.trackSideObject(this.add.rectangle(x, y, width, 30, 0x0b1118, 0.96));
|
||||||
|
background.setOrigin(0);
|
||||||
|
background.setDepth(30);
|
||||||
|
background.setStrokeStyle(1, 0x647485, 0.62);
|
||||||
|
|
||||||
|
const renderControl = (controlX: number, label: string, enabled: boolean, nextPage: number) => {
|
||||||
|
const controlWidth = 72;
|
||||||
|
const button = this.trackSideObject(this.add.rectangle(controlX, y + 3, controlWidth, 24, enabled ? 0x25384a : 0x141b22, 0.96));
|
||||||
|
button.setOrigin(0);
|
||||||
|
button.setDepth(31);
|
||||||
|
button.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.78 : 0.36);
|
||||||
|
if (enabled) {
|
||||||
|
button.setInteractive({ useHandCursor: true });
|
||||||
|
button.on('pointerdown', () => {
|
||||||
|
this.rosterPage = nextPage;
|
||||||
|
this.renderRosterPanel(this.rosterTab, message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.trackSideObject(this.add.text(controlX + controlWidth / 2, y + 15, label, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: enabled ? '#f4dfad' : '#77828c',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
text.setDepth(32);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderControl(x + 3, '이전', this.rosterPage > 0, this.rosterPage - 1);
|
||||||
|
renderControl(x + width - 75, '다음', this.rosterPage < pageCount - 1, this.rosterPage + 1);
|
||||||
|
|
||||||
|
const indicator = this.trackSideObject(this.add.text(x + width / 2, y + 15, `${this.rosterPage + 1} / ${pageCount}`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#d4dce6',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
indicator.setOrigin(0.5);
|
||||||
|
indicator.setDepth(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRosterRow(unit: UnitData, x: number, y: number, width: number) {
|
private renderRosterRow(unit: UnitData, x: number, y: number, width: number) {
|
||||||
@@ -22302,6 +22556,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.phase = 'idle';
|
this.phase = 'idle';
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
|
this.updateMiniMap();
|
||||||
this.refreshUnitLegibilityStyles();
|
this.refreshUnitLegibilityStyles();
|
||||||
this.renderRosterPanel(tab);
|
this.renderRosterPanel(tab);
|
||||||
}
|
}
|
||||||
@@ -22309,6 +22564,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private clearSidePanelContent() {
|
private clearSidePanelContent() {
|
||||||
this.sidePanelObjects.forEach((object) => object.destroy());
|
this.sidePanelObjects.forEach((object) => object.destroy());
|
||||||
this.sidePanelObjects = [];
|
this.sidePanelObjects = [];
|
||||||
|
this.recentActionLogLayout = undefined;
|
||||||
|
this.rosterPanelLayout = undefined;
|
||||||
this.bondChainReadyFocusedPreview = undefined;
|
this.bondChainReadyFocusedPreview = undefined;
|
||||||
this.clearFacingIndicator();
|
this.clearFacingIndicator();
|
||||||
}
|
}
|
||||||
@@ -22494,6 +22751,80 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private battleHudDebugState() {
|
||||||
|
const miniMap = this.miniMapLayout;
|
||||||
|
const selectedDot = this.selectedUnit ? this.miniMapUnitDots.get(this.selectedUnit.id) : undefined;
|
||||||
|
const objectiveDot = this.miniMapUnitDots.get(leaderUnitId);
|
||||||
|
const recentActions = this.recentActionLogLayout
|
||||||
|
? {
|
||||||
|
...this.recentActionLogLayout,
|
||||||
|
bottom: this.recentActionLogLayout.y + this.recentActionLogLayout.height,
|
||||||
|
gapToMiniMap: miniMap
|
||||||
|
? miniMap.frameY - (this.recentActionLogLayout.y + this.recentActionLogLayout.height)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const rosterPanel = this.rosterPanelLayout
|
||||||
|
? {
|
||||||
|
...this.rosterPanelLayout,
|
||||||
|
visibleUnitIds: [...this.rosterPanelLayout.visibleUnitIds],
|
||||||
|
listBounds: { ...this.rosterPanelLayout.listBounds },
|
||||||
|
navigationBounds: this.rosterPanelLayout.navigationBounds
|
||||||
|
? { ...this.rosterPanelLayout.navigationBounds }
|
||||||
|
: null,
|
||||||
|
messageBounds: this.rosterPanelLayout.messageBounds
|
||||||
|
? { ...this.rosterPanelLayout.messageBounds }
|
||||||
|
: null,
|
||||||
|
gapToMiniMap: miniMap
|
||||||
|
? miniMap.frameY - Math.max(
|
||||||
|
this.rosterPanelLayout.listBounds.y + this.rosterPanelLayout.listBounds.height,
|
||||||
|
this.rosterPanelLayout.navigationBounds
|
||||||
|
? this.rosterPanelLayout.navigationBounds.y + this.rosterPanelLayout.navigationBounds.height
|
||||||
|
: 0,
|
||||||
|
this.rosterPanelLayout.messageBounds
|
||||||
|
? this.rosterPanelLayout.messageBounds.y + this.rosterPanelLayout.messageBounds.height
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
miniMap: miniMap
|
||||||
|
? {
|
||||||
|
visible: this.miniMapVisible,
|
||||||
|
title: '전장 지도',
|
||||||
|
legend: ['아', '적', '목'],
|
||||||
|
frameBounds: {
|
||||||
|
x: miniMap.frameX,
|
||||||
|
y: miniMap.frameY,
|
||||||
|
width: miniMap.frameWidth,
|
||||||
|
height: miniMap.frameHeight
|
||||||
|
},
|
||||||
|
headerBounds: {
|
||||||
|
x: miniMap.frameX,
|
||||||
|
y: miniMap.frameY,
|
||||||
|
width: miniMap.frameWidth,
|
||||||
|
height: miniMap.headerHeight
|
||||||
|
},
|
||||||
|
mapBounds: { x: miniMap.x, y: miniMap.y, width: miniMap.width, height: miniMap.height },
|
||||||
|
viewportBounds: this.gameObjectBoundsDebug(this.miniMapViewport),
|
||||||
|
cellSize: miniMap.cellSize,
|
||||||
|
objectiveUnitId: leaderUnitId,
|
||||||
|
objectiveHighlighted: Boolean(
|
||||||
|
objectiveDot?.visible && objectiveDot.lineWidth === 2 && objectiveDot.strokeColor === palette.gold
|
||||||
|
),
|
||||||
|
selectedUnitId: this.selectedUnit?.id ?? null,
|
||||||
|
selectedHighlighted: Boolean(
|
||||||
|
selectedDot?.visible && selectedDot.lineWidth === 2 && selectedDot.strokeColor === 0xffffff
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
recentActions,
|
||||||
|
rosterPanel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getDebugState() {
|
getDebugState() {
|
||||||
const campaign = getCampaignState();
|
const campaign = getCampaignState();
|
||||||
const effectiveSortieUnitIds = this.effectiveSortieUnitIds(campaign);
|
const effectiveSortieUnitIds = this.effectiveSortieUnitIds(campaign);
|
||||||
@@ -22520,6 +22851,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
sortieItemAssignments,
|
sortieItemAssignments,
|
||||||
sortieOrder: sortieOrderDebug,
|
sortieOrder: sortieOrderDebug,
|
||||||
sortieOperationOrder: sortieOrderDebug,
|
sortieOperationOrder: sortieOrderDebug,
|
||||||
|
battleHud: this.battleHudDebugState(),
|
||||||
sortieDoctrine: this.debugSortieDoctrine(),
|
sortieDoctrine: this.debugSortieDoctrine(),
|
||||||
firstSortieRoleEffects: this.debugFirstPursuitRoleEffects(),
|
firstSortieRoleEffects: this.debugFirstPursuitRoleEffects(),
|
||||||
firstSortieRoleMechanicsProbe: this.debugFirstPursuitRoleMechanicsProbe(),
|
firstSortieRoleMechanicsProbe: this.debugFirstPursuitRoleMechanicsProbe(),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const config: Phaser.Types.Core.GameConfig = {
|
|||||||
height: 720,
|
height: 720,
|
||||||
backgroundColor: '#10131a',
|
backgroundColor: '#10131a',
|
||||||
scale: {
|
scale: {
|
||||||
mode: Phaser.Scale.RESIZE,
|
mode: Phaser.Scale.FIT,
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH
|
autoCenter: Phaser.Scale.CENTER_BOTH
|
||||||
},
|
},
|
||||||
scene: [BootScene, TitleScene]
|
scene: [BootScene, TitleScene]
|
||||||
|
|||||||
Reference in New Issue
Block a user