diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 193f57a..1343985 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -390,7 +390,21 @@ try { 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); const cameraAfterMiniMapClick = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.camera); if ( @@ -398,7 +412,7 @@ try { cameraAfterMiniMapClick.x < cameraAfterMiniMapClick.mapWidth - cameraAfterMiniMapClick.visibleColumns || 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); @@ -9931,7 +9945,7 @@ async function ensureSortiePrepStep(page, expectedStep) { } 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) { throw new Error(`Cannot launch sortie; preparation is not visible: ${JSON.stringify(state)}`); } @@ -9941,13 +9955,35 @@ async function launchSortieFromCurrentStep(page) { 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 page.waitForTimeout(160); await page.keyboard.press('Enter'); - await page.waitForFunction(() => { - const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; - const camp = window.__HEROS_DEBUG__?.camp(); - return !activeScenes.includes('CampScene') || camp?.sortieVisible !== true; - }, undefined, { timeout: 30000 }); + try { + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + const camp = window.__HEROS_DEBUG__?.camp(); + 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) { diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index 5732ff0..393f07c 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -41,6 +41,9 @@ try { await page.reload({ waitUntil: 'domcontentloaded' }); await waitForTitle(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 assertCanvasPainted(page, 'title first run'); @@ -60,16 +63,36 @@ try { const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene'); const state = window.__HEROS_DEBUG__?.battle(); 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 { battleId: state?.battleId, objectiveText: scene?.objectiveTrackerText?.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 = []) { 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.objectiveText?.startsWith('승리 목표:'), `Expected clear victory objective label: ${JSON.stringify(firstBattleProbe)}`); @@ -85,6 +108,50 @@ try { !firstBattleProbe.sideTexts.some((text) => text.includes('...')), `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'); const firstBattleBondChain = await page.evaluate(() => { 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)}`); } +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) { await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 2f7036d..1018248 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1131,6 +1131,32 @@ type MiniMapLayout = { width: number; height: 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 = { @@ -3336,6 +3362,8 @@ export class BattleScene extends Phaser.Scene { private markers: Phaser.GameObjects.Rectangle[] = []; private objectiveMarkerObjects: Phaser.GameObjects.GameObject[] = []; private rosterTab: RosterTab = 'ally'; + private rosterPage = 0; + private rosterPanelLayout?: RosterPanelLayout; private sidePanelObjects: Phaser.GameObjects.GameObject[] = []; private commandButtons: CommandButton[] = []; private commandMenuObjects: Phaser.GameObjects.GameObject[] = []; @@ -3424,6 +3452,7 @@ export class BattleScene extends Phaser.Scene { private miniMapUnitDots = new Map(); private miniMapViewport?: Phaser.GameObjects.Rectangle; private miniMapVisible = true; + private recentActionLogLayout?: RecentActionLogLayout; private cameraTileX = 0; private cameraTileY = 0; private edgeScrollElapsed = 0; @@ -3445,6 +3474,9 @@ export class BattleScene extends Phaser.Scene { init(data?: BattleSceneData) { configureBattleScenario(getBattleScenario(data?.battleId)); + this.rosterTab = 'ally'; + this.rosterPage = 0; + this.rosterPanelLayout = undefined; const campaign = getCampaignState(); const selectedOrderId = campaign.sortieOrderSelection?.battleId === battleScenario.id ? campaign.sortieOrderSelection.orderId @@ -5531,7 +5563,7 @@ export class BattleScene extends Phaser.Scene { const { panelX, panelY, panelWidth, panelHeight } = this.layout; 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 width = panelWidth - 48; const height = 128; @@ -5589,20 +5621,76 @@ export class BattleScene extends Phaser.Scene { private drawMiniMap() { const { panelX, panelY, panelWidth, panelHeight } = this.layout; const maxWidth = panelWidth - 48; - const maxHeight = 132; - const cellSize = Math.floor(Math.min(maxWidth / battleMap.width, maxHeight / battleMap.height)); + const maxHeight = 108; + const cellSize = Math.max(1, Math.floor(Math.min(maxWidth / battleMap.width, maxHeight / battleMap.height))); const width = battleMap.width * cellSize; const height = battleMap.height * cellSize; const x = panelX + Math.floor((panelWidth - width) / 2); - const y = panelY + panelHeight - height - 24; - this.miniMapLayout = { x, y, width, height, cellSize }; + const y = panelY + panelHeight - height - 12; + 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.setStrokeStyle(2, palette.gold, 0.72); + frame.setStrokeStyle(1, palette.gold, 0.78); frame.setDepth(24); + frame.setName('mini-map-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); hitArea.setOrigin(0); hitArea.setInteractive({ useHandCursor: true }); @@ -5790,9 +5878,21 @@ export class BattleScene extends Phaser.Scene { if (!unit) { 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.setDisplaySize(markerSize, markerSize); dot.setVisible(unit.hp > 0); 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); @@ -6369,6 +6469,7 @@ export class BattleScene extends Phaser.Scene { this.pendingMove = undefined; this.phase = 'idle'; this.hideCommandMenu(); + this.updateMiniMap(); this.refreshUnitLegibilityStyles(); this.showEnemyUnitThreatRange(unit); this.renderUnitDetail(unit, this.enemyDetailMessage(unit)); @@ -6385,6 +6486,7 @@ export class BattleScene extends Phaser.Scene { this.clearMarkers(); this.hideCommandMenu(); this.phase = 'idle'; + this.updateMiniMap(); this.refreshUnitLegibilityStyles(); this.renderUnitDetail(unit, '이미 행동을 마친 장수입니다.'); return; @@ -6396,6 +6498,7 @@ export class BattleScene extends Phaser.Scene { this.clearMarkers(); this.hideCommandMenu(); this.showMoveRange(unit); + this.updateMiniMap(); this.refreshUnitLegibilityStyles(); this.renderUnitDetail(unit, '이동할 파란 칸을 선택하세요.'); } @@ -21077,37 +21180,52 @@ export class BattleScene extends Phaser.Scene { private sideContentBottom(padding = 16) { 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) { + 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)); bg.setOrigin(0); bg.setDepth(30); 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', - fontSize: '15px', + fontSize: compact ? '14px' : '15px', color: '#f2e3bf', fontStyle: '700' })); 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', - fontSize: '12px', + fontSize: compact ? '11px' : '12px', color: '#9fb0bf', fontStyle: '700' })); countText.setOrigin(1, 0); 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) { - 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', - fontSize: '13px', + fontSize: compact ? '12px' : '13px', color: '#9fb0bf' })); emptyText.setDepth(31); @@ -21115,35 +21233,38 @@ export class BattleScene extends Phaser.Scene { } entries.forEach((entry, index) => { - const rowY = y + 32 + index * 28; + const rowY = y + headerHeight + index * rowStep; const { turn, text } = this.battleLogDisplayParts(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.setDepth(31); 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.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); - 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', - fontSize: '10px', + fontSize: compact ? '9px' : '10px', color: '#9fb0bf', fontStyle: '700' })); turnText.setOrigin(1, 0); 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', - fontSize: index === 0 ? '13px' : '12px', + fontSize: compact ? (index === 0 ? '12px' : '11px') : index === 0 ? '13px' : '12px', color: index === 0 ? '#e8dfca' : '#c8d2dd', fontStyle: index === 0 ? '700' : '400', - fixedWidth: width - 92 + fixedWidth: width - (compact ? 84 : 92) })); eventText.setDepth(32); }); @@ -21218,8 +21339,8 @@ export class BattleScene extends Phaser.Scene { const allyCount = battleUnits.filter((unit) => unit.faction === 'ally').length; const enemyCount = battleUnits.filter((unit) => unit.faction === 'enemy' && unit.hp > 0).length; const objectiveStates = this.objectiveStates(this.battleOutcome); - const logHeight = 118; - const logTop = Math.max(top + 236, this.sideContentBottom(10) - logHeight); + const logPlacement = this.recentActionLogPlacement(top + 232); + const logTop = logPlacement?.y ?? this.sideContentBottom(6); const messageTop = top + 168; const messageHeight = Math.max(52, Math.min(72, logTop - messageTop - 12)); @@ -21236,15 +21357,23 @@ export class BattleScene extends Phaser.Scene { if (message) { 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; } - 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 objectiveHeight = logTop - objectiveTop - 8; - this.renderObjectiveStateGroups(objectiveStates, left, objectiveTop, width, Math.max(72, logTop - objectiveTop - 8)); - this.renderRecentActionLogPanel(left, logTop, width, logHeight); + if (objectiveHeight >= 58) { + this.renderObjectiveStateGroups(objectiveStates, left, objectiveTop, width, objectiveHeight); + } + if (logPlacement) { + this.renderRecentActionLogPanel(left, logPlacement.y, width, logPlacement.height); + } } private compactSituationMessage(message: string) { @@ -21255,26 +21384,44 @@ export class BattleScene extends Phaser.Scene { .join('\n'); } - private renderTacticalGuideCard(x: number, y: number, width: number) { + private renderTacticalGuideCard(x: number, y: number, width: number, compact = false) { const guide = battleScenario.tacticalGuide; if (!guide) { return 0; } - const height = 96; + const height = compact ? 56 : 96; const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x101820, 0.92)); bg.setOrigin(0); bg.setDepth(30); 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', - fontSize: '15px', + fontSize: compact ? '14px' : '15px', color: '#f2e3bf', fontStyle: '700' })); 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), { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', @@ -21619,7 +21766,11 @@ export class BattleScene extends Phaser.Scene { } private renderRosterPanel(tab: RosterTab = this.rosterTab, message?: string) { + const tabChanged = this.rosterTab !== tab; this.rosterTab = tab; + if (tabChanged) { + this.rosterPage = 0; + } this.clearSidePanelContent(); this.setMiniMapVisible(true); @@ -21650,23 +21801,126 @@ export class BattleScene extends Phaser.Scene { const units = battleUnits.filter((unit) => unit.faction === tab); const listTop = top + 54; - units.forEach((unit, index) => { - this.renderRosterRow(unit, left, listTop + index * 50, width); + const contentBottom = this.sideContentBottom(); + 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 logTop = this.sideContentBottom(10) - logHeight; - const listBottom = listTop + units.length * 50; - const canShowLog = listBottom + 12 < logTop; - if (message) { - const messageHeight = canShowLog ? 70 : 104; - const messageBottomLimit = canShowLog ? logTop - 10 : panelY + panelHeight - 124; - const messageTop = Math.min(messageBottomLimit - messageHeight, listBottom + 14); - this.renderPanelMessage(message, left, messageTop, width, messageHeight); - } - if (canShowLog) { - this.renderRecentActionLogPanel(left, logTop, width, logHeight); + const listBottom = listTop + visibleUnits.length * rowStep; + if (pageCount > 1) { + const navigationY = listBottom + 4; + navigationBounds = { x: left, y: navigationY, width, height: 30 }; + this.renderRosterPageNavigation(left, navigationY, width, pageCount, message); + + if (message) { + const messageY = navigationY + navigationBounds.height + 8; + const messageHeight = Math.min(52, contentBottom - messageY); + if (messageHeight >= 44) { + this.renderPanelMessage(message, left, messageY, width, messageHeight); + 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) { @@ -22302,6 +22556,7 @@ export class BattleScene extends Phaser.Scene { this.phase = 'idle'; this.clearMarkers(); this.hideCommandMenu(); + this.updateMiniMap(); this.refreshUnitLegibilityStyles(); this.renderRosterPanel(tab); } @@ -22309,6 +22564,8 @@ export class BattleScene extends Phaser.Scene { private clearSidePanelContent() { this.sidePanelObjects.forEach((object) => object.destroy()); this.sidePanelObjects = []; + this.recentActionLogLayout = undefined; + this.rosterPanelLayout = undefined; this.bondChainReadyFocusedPreview = undefined; 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() { const campaign = getCampaignState(); const effectiveSortieUnitIds = this.effectiveSortieUnitIds(campaign); @@ -22520,6 +22851,7 @@ export class BattleScene extends Phaser.Scene { sortieItemAssignments, sortieOrder: sortieOrderDebug, sortieOperationOrder: sortieOrderDebug, + battleHud: this.battleHudDebugState(), sortieDoctrine: this.debugSortieDoctrine(), firstSortieRoleEffects: this.debugFirstPursuitRoleEffects(), firstSortieRoleMechanicsProbe: this.debugFirstPursuitRoleMechanicsProbe(), diff --git a/src/main.ts b/src/main.ts index 476aeda..5bf7765 100644 --- a/src/main.ts +++ b/src/main.ts @@ -50,7 +50,7 @@ const config: Phaser.Types.Core.GameConfig = { height: 720, backgroundColor: '#10131a', scale: { - mode: Phaser.Scale.RESIZE, + mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, scene: [BootScene, TitleScene]