From 7125e66e701e16a80d200695a27ffaf5c36da15b Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 9 Aug 2026 14:46:12 +0000 Subject: [PATCH] fix(game-frontend): preserve castle icon geometry --- app/game-frontend/e2e/inGameInfo.spec.ts | 98 +++++++++++++++++-- .../src/components/main/MapCityDetail.vue | 2 +- .../src/components/main/MapViewer.vue | 7 +- app/game-frontend/src/stores/mainDashboard.ts | 6 +- 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/app/game-frontend/e2e/inGameInfo.spec.ts b/app/game-frontend/e2e/inGameInfo.spec.ts index a2c34ae1..8d0cfd0c 100644 --- a/app/game-frontend/e2e/inGameInfo.spec.ts +++ b/app/game-frontend/e2e/inGameInfo.spec.ts @@ -55,13 +55,23 @@ const city = { incomes: { gold: 1000, rice: 900, wall: 800 }, officers: { 2: null, 3: null, 4: { id: 1, name: '태수', npcState: 0, officerLevel: 4, cityId: 1, cityName: '업' } }, }; +const castleFixtures = [ + { id: 1, level: 8, layoutLevel: 1, x: 100, y: 100, width: 32, height: 24 }, + { id: 2, level: 1, layoutLevel: 8, x: 200, y: 100, width: 16, height: 15 }, + { id: 3, level: 2, layoutLevel: 8, x: 300, y: 100, width: 20, height: 14 }, + { id: 4, level: 3, layoutLevel: 8, x: 400, y: 100, width: 14, height: 14 }, + { id: 5, level: 4, layoutLevel: 8, x: 100, y: 220, width: 20, height: 15 }, + { id: 6, level: 5, layoutLevel: 8, x: 200, y: 220, width: 24, height: 16 }, + { id: 7, level: 6, layoutLevel: 8, x: 300, y: 220, width: 26, height: 18 }, + { id: 8, level: 7, layoutLevel: 8, x: 400, y: 220, width: 28, height: 20 }, +] as const; const map = { result: true, version: 0, startYear: 180, year: 200, month: 1, - cityList: [[1, 8, 0, 1, 1, 1]], + cityList: castleFixtures.map(({ id, level }) => [id, level, 0, 1, 1, 1]), nationList: [[1, '아국', '#008000', 1]], spyList: {}, shownByGeneralList: [], @@ -70,9 +80,17 @@ const map = { }; const layout = { mapName: 'che', - cityList: [{ id: 1, name: '업', level: 8, region: 1, x: 345, y: 130, path: [] }], + cityList: castleFixtures.map(({ id, layoutLevel: level, x, y }) => ({ + id, + name: id === 1 ? '업' : `성${id}`, + level, + region: 1, + x, + y, + path: [], + })), regionMap: { 1: '하북' }, - levelMap: { 8: '특' }, + levelMap: { 1: '수', 2: '진', 3: '관', 4: '이', 5: '소', 6: '중', 7: '대', 8: '특' }, }; const generalContext = { general: { @@ -357,6 +375,40 @@ test('global-info renders the ref nation summary columns beside the map', async await page.setViewportSize({ width: 1200, height: 900 }); await go(page, 'global-info'); + const castleGeometry = await page.locator('.map-area').evaluate((mapArea) => { + const mapRect = mapArea.getBoundingClientRect(); + return Array.from(mapArea.querySelectorAll('.city-icon')).map((image) => { + const iconRect = image.getBoundingClientRect(); + const cityBase = image.closest('.city-base'); + if (!cityBase) throw new Error('castle icon is missing its city coordinate cell'); + const baseRect = cityBase.getBoundingClientRect(); + return { + src: new URL(image.src).pathname, + naturalWidth: image.naturalWidth, + naturalHeight: image.naturalHeight, + width: iconRect.width, + height: iconRect.height, + iconCenterX: iconRect.left + iconRect.width / 2 - mapRect.left, + iconCenterY: iconRect.top + iconRect.height / 2 - mapRect.top, + cellCenterX: baseRect.left + baseRect.width / 2 - mapRect.left, + cellCenterY: baseRect.top + baseRect.height / 2 - mapRect.top, + }; + }); + }); + expect(castleGeometry).toEqual( + castleFixtures.map(({ level, x, y, width, height }) => ({ + src: `/game/cast_${level}.gif`, + naturalWidth: width, + naturalHeight: height, + width, + height, + iconCenterX: x, + iconCenterY: y, + cellCenterX: x, + cellCenterY: y, + })) + ); + const summary = page.locator('.simple-nation-list'); await expect(summary).toBeVisible(); await expect(summary.locator('thead')).toContainText('국명'); @@ -368,7 +420,9 @@ test('global-info renders the ref nation summary columns beside the map', async const geometry = await summary.evaluate((element) => { const rect = element.getBoundingClientRect(); - const headings = Array.from(element.querySelectorAll('th')).map((heading) => heading.getBoundingClientRect().width); + const headings = Array.from(element.querySelectorAll('th')).map( + (heading) => heading.getBoundingClientRect().width + ); return { x: rect.x, width: rect.width, headings }; }); expect(geometry).toMatchObject({ x: 800, width: 300 }); @@ -384,11 +438,12 @@ test('global-info renders the ref nation summary columns beside the map', async `${JSON.stringify( { geometry, + castleGeometry, headings: await summary.locator('th').allTextContents(), rows: await summary.locator('tbody tr').allTextContents(), - cityTitles: await summary.locator('tbody td:last-child').evaluateAll((cells) => - cells.map((cell) => cell.getAttribute('title')) - ), + cityTitles: await summary + .locator('tbody td:last-child') + .evaluateAll((cells) => cells.map((cell) => cell.getAttribute('title'))), }, null, 2 @@ -399,6 +454,30 @@ test('global-info renders the ref nation summary columns beside the map', async } await page.setViewportSize({ width: 390, height: 844 }); + const mobileCastleGeometry = await page.locator('.map-area').evaluate((mapArea) => { + const mapRect = mapArea.getBoundingClientRect(); + return Array.from(mapArea.querySelectorAll('.city-icon')).map((image) => { + const rect = image.getBoundingClientRect(); + return { + naturalWidth: image.naturalWidth, + naturalHeight: image.naturalHeight, + width: rect.width, + height: rect.height, + centerX: rect.left + rect.width / 2 - mapRect.left, + centerY: rect.top + rect.height / 2 - mapRect.top, + }; + }); + }); + const smallScale = 5 / 7; + for (const [index, rendered] of mobileCastleGeometry.entries()) { + const { width, height, x, y } = castleFixtures[index]!; + expect(rendered.naturalWidth).toBe(width); + expect(rendered.naturalHeight).toBe(height); + expect(rendered.width).toBeCloseTo(width * smallScale, 1); + expect(rendered.height).toBeCloseTo(height * smallScale, 1); + expect(rendered.centerX).toBeCloseTo(x * smallScale, 1); + expect(rendered.centerY).toBeCloseTo(y * smallScale, 1); + } const mobileGeometry = await page.locator('.map-grid').evaluate((element) => { const map = element.querySelector('.map-viewer')?.getBoundingClientRect(); const summary = element.querySelector('.simple-nation-list')?.getBoundingClientRect(); @@ -514,7 +593,10 @@ test('current-city renders a missing merchant rate with the legacy dash and perc await page.setViewportSize({ width: 1200, height: 900 }); await go(page, 'current-city'); - const tradeValue = page.locator('.stats th').filter({ hasText: /^시세$/ }).locator('xpath=following-sibling::td[1]'); + const tradeValue = page + .locator('.stats th') + .filter({ hasText: /^시세$/ }) + .locator('xpath=following-sibling::td[1]'); await expect(tradeValue).toHaveText('- %'); }); diff --git a/app/game-frontend/src/components/main/MapCityDetail.vue b/app/game-frontend/src/components/main/MapCityDetail.vue index 1c141ad9..4fad1b55 100644 --- a/app/game-frontend/src/components/main/MapCityDetail.vue +++ b/app/game-frontend/src/components/main/MapCityDetail.vue @@ -159,7 +159,7 @@ const cityStateStyle = computed(() => ({ @click.stop="emit('select', props.city.id)" >
-
+
diff --git a/app/game-frontend/src/components/main/MapViewer.vue b/app/game-frontend/src/components/main/MapViewer.vue index 78290f3e..ba8b6ab9 100644 --- a/app/game-frontend/src/components/main/MapViewer.vue +++ b/app/game-frontend/src/components/main/MapViewer.vue @@ -157,7 +157,8 @@ const cityViews = computed(() => { return props.mapLayout.cityList.map((layoutCity) => { const dynamic = dynamicCityById.value.get(layoutCity.id); - const [, state = 0, nationId = 0, region = layoutCity.region, supplyFlag = 0] = dynamic ?? []; + const [level = layoutCity.level, state = 0, nationId = 0, region = layoutCity.region, supplyFlag = 0] = + dynamic ?? []; const nation = nationById.value.get(nationId); const x = layoutCity.x * scale; const y = layoutCity.y * scale; @@ -165,8 +166,8 @@ const cityViews = computed(() => { return { id: layoutCity.id, name: layoutCity.name, - level: layoutCity.level, - levelName: props.mapLayout?.levelMap?.[layoutCity.level] ?? '-', + level, + levelName: props.mapLayout?.levelMap?.[level] ?? '-', state, stateClass: resolveStateClass(state), nationId, diff --git a/app/game-frontend/src/stores/mainDashboard.ts b/app/game-frontend/src/stores/mainDashboard.ts index 47ad8bda..763d564a 100644 --- a/app/game-frontend/src/stores/mainDashboard.ts +++ b/app/game-frontend/src/stores/mainDashboard.ts @@ -87,15 +87,15 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => { if (!layoutCity || !mapEntry) { return null; } - const [, , state, nationIdValue, region, supplyFlag] = mapEntry; + const [, level, state, nationIdValue, region, supplyFlag] = mapEntry; const nationEntry = map.nationList.find((nationEntry) => nationEntry[0] === nationIdValue); const regionName = layout.regionMap[region] ?? '-'; - const levelName = layout.levelMap[layoutCity.level] ?? '-'; + const levelName = layout.levelMap[level] ?? '-'; return { id: layoutCity.id, name: layoutCity.name, - level: layoutCity.level, + level, levelName, region, regionName,