fix(game-frontend): preserve castle icon geometry

This commit is contained in:
2026-08-09 14:46:17 +00:00
parent 00b9fcdb93
commit 7125e66e70
4 changed files with 98 additions and 15 deletions
+90 -8
View File
@@ -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<HTMLImageElement>('.city-icon')).map((image) => {
const iconRect = image.getBoundingClientRect();
const cityBase = image.closest<HTMLElement>('.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<HTMLImageElement>('.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('- %');
});
@@ -159,7 +159,7 @@ const cityStateStyle = computed(() => ({
@click.stop="emit('select', props.city.id)"
>
<div v-if="cityBgStyle" class="city-bg" :style="[cityBgWrapperStyle, cityBgStyle]" />
<div class="city-img">
<div class="city-img" :style="cityIconStyle">
<img class="city-icon" :src="castleIcon" :style="cityIconStyle" />
<div class="city-filler" :class="{ 'my-city': props.city.isMyCity }" />
<div v-if="flagIcon" class="city-flag" :style="cityFlagStyle">
@@ -157,7 +157,8 @@ const cityViews = computed<CityView[]>(() => {
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<CityView[]>(() => {
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,
@@ -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,