fix(game-ui): keep current city visible in command maps
This commit is contained in:
@@ -465,6 +465,7 @@ test('enters general and nation command arguments and sends exact values', async
|
||||
const requests = await install(page);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await page.goto('/');
|
||||
await expect(page.getByTestId('current-city-marker')).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||
await page.getByTestId('command-picker').getByRole('button', { name: /화계/ }).click();
|
||||
@@ -474,6 +475,12 @@ test('enters general and nation command arguments and sends exact values', async
|
||||
await expect(form.getByTestId('command-argument-guidance')).toContainText('선택한 도시에 화계를 실행합니다.');
|
||||
await expect(form.getByTestId('command-map-target-summary')).toContainText('현재 도시에서 0칸');
|
||||
const commandMap = form.getByTestId('command-argument-map');
|
||||
const currentCityMarker = commandMap.getByTestId('current-city-marker');
|
||||
const selectionStatus = form.getByTestId('command-map-selection-status');
|
||||
await expect(currentCityMarker).toHaveText('현재');
|
||||
await expect(currentCityMarker).toHaveAttribute('aria-label', '현재 도시 업');
|
||||
await expect(selectionStatus).toContainText('현재 도시업');
|
||||
await expect(selectionStatus).toContainText('선택 도시업');
|
||||
const mapCities = commandMap.locator('.city-base');
|
||||
await expect(mapCities).toHaveCount(2);
|
||||
await expect(commandMap.locator('.city-bg')).toHaveCount(2);
|
||||
@@ -515,11 +522,17 @@ test('enters general and nation command arguments and sends exact values', async
|
||||
await mapCities.nth(1).click();
|
||||
await expect(form.locator('select')).toHaveValue('2');
|
||||
await expect(form.getByTestId('command-map-target-summary')).toContainText('현재 도시에서 1칸');
|
||||
await expect(selectionStatus).toContainText('현재 도시업');
|
||||
await expect(selectionStatus).toContainText('선택 도시허창');
|
||||
await expect(currentCityMarker).toHaveAttribute('aria-label', '현재 도시 업');
|
||||
await expect(mapCities.nth(0)).toHaveClass(/mine/);
|
||||
await expect(mapCities.nth(1)).toHaveClass(/selected/);
|
||||
await mapCities.nth(1).hover();
|
||||
expect(await mapCities.nth(1).evaluate((element) => getComputedStyle(element).cursor)).toBe('pointer');
|
||||
await mapCities.nth(1).focus();
|
||||
await expect(mapCities.nth(1)).toBeFocused();
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
await commandMap.screenshot({ path: test.info().outputPath('main-city-current-marker-desktop.png') });
|
||||
const mapGeometry = await form.getByTestId('command-argument-map').evaluate((element) => {
|
||||
const area = element.querySelector<HTMLElement>('.map-area')!;
|
||||
const rect = area.getBoundingClientRect();
|
||||
@@ -685,6 +698,9 @@ test('uses the map to choose a nation target in the chief command window', async
|
||||
await expect(form.getByTestId('command-argument-guidance')).toContainText('초반 제한');
|
||||
await form.getByTestId('command-argument-map').locator('.city-base').nth(1).click();
|
||||
await expect(form.locator('select')).toHaveValue('2');
|
||||
await expect(form.getByTestId('command-map-selection-status')).toContainText('현재 도시업');
|
||||
await expect(form.getByTestId('command-map-selection-status')).toContainText('선택 국가적국');
|
||||
await expect(form.getByTestId('current-city-marker')).toHaveAttribute('aria-label', '현재 도시 업');
|
||||
await expect(form.getByTestId('command-map-target-summary')).toContainText('수도 허창 · 도시 1개');
|
||||
await expect(page).toHaveURL(/\/che\/chief-center$/);
|
||||
await page.screenshot({ path: test.info().outputPath('chief-nation-map-option.png'), fullPage: true });
|
||||
@@ -697,6 +713,16 @@ test('fits the city map option window inside the Ref-compatible 500px mobile pag
|
||||
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||
const picker = page.getByTestId('command-picker');
|
||||
await picker.getByRole('button', { name: /화계/ }).click();
|
||||
const mobileForm = picker.getByTestId('command-argument-form');
|
||||
const mobileMap = mobileForm.getByTestId('command-argument-map');
|
||||
const mobileCities = mobileMap.locator('.city-base');
|
||||
await mobileCities.nth(1).click();
|
||||
await expect(mobileForm.locator('select')).toHaveValue('2');
|
||||
await expect(mobileForm.getByTestId('command-map-selection-status')).toContainText('현재 도시업');
|
||||
await expect(mobileForm.getByTestId('command-map-selection-status')).toContainText('선택 도시허창');
|
||||
await expect(mobileMap.getByTestId('current-city-marker')).toHaveAttribute('aria-label', '현재 도시 업');
|
||||
await expect(mobileCities.nth(0)).toHaveClass(/mine/);
|
||||
await expect(mobileCities.nth(1)).toHaveClass(/selected/);
|
||||
const geometry = await picker.evaluate((element) => {
|
||||
const map = element.querySelector<HTMLElement>('[data-testid="command-argument-map"] .map-area')!;
|
||||
const pickerRect = element.getBoundingClientRect();
|
||||
@@ -708,6 +734,19 @@ test('fits the city map option window inside the Ref-compatible 500px mobile pag
|
||||
pickerScrollWidth: element.scrollWidth,
|
||||
mapWidth: mapRect.width,
|
||||
mapHeight: mapRect.height,
|
||||
marker: (() => {
|
||||
const marker = element.querySelector<HTMLElement>('[data-testid="current-city-marker"]')!;
|
||||
const rect = marker.getBoundingClientRect();
|
||||
const style = getComputedStyle(marker);
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
pointerEvents: style.pointerEvents,
|
||||
borderColor: style.borderColor,
|
||||
};
|
||||
})(),
|
||||
};
|
||||
});
|
||||
expect(geometry.pickerX).toBeGreaterThanOrEqual(0);
|
||||
@@ -716,6 +755,12 @@ test('fits the city map option window inside the Ref-compatible 500px mobile pag
|
||||
expect(geometry.pickerScrollWidth).toBeLessThanOrEqual(geometry.pickerWidth);
|
||||
expect(geometry.mapWidth).toBeGreaterThan(470);
|
||||
expect(geometry.mapHeight / geometry.mapWidth).toBeCloseTo(5 / 7, 2);
|
||||
expect(geometry.marker.left).toBeGreaterThanOrEqual(0);
|
||||
expect(geometry.marker.right).toBeLessThanOrEqual(500);
|
||||
expect(geometry.marker.top).toBeGreaterThanOrEqual(0);
|
||||
expect(geometry.marker.bottom).toBeLessThanOrEqual(900);
|
||||
expect(geometry.marker.pointerEvents).toBe('none');
|
||||
expect(geometry.marker.borderColor).toBe('rgb(130, 207, 255)');
|
||||
await page.screenshot({ path: test.info().outputPath('main-city-map-option-mobile.png'), fullPage: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -109,6 +109,26 @@ const mapSelectedCityId = computed<number | null>(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
const currentCityName = computed(() => {
|
||||
const cityId = props.mapData?.myCity;
|
||||
if (!cityId) return '-';
|
||||
return props.mapLayout?.cityList.find((city) => city.id === cityId)?.name ?? '-';
|
||||
});
|
||||
|
||||
const selectedMapTargetName = computed(() => {
|
||||
if (presentation.value.mapTarget === 'city') {
|
||||
const cityId = mapSelectedCityId.value;
|
||||
if (!cityId) return '-';
|
||||
return props.mapLayout?.cityList.find((city) => city.id === cityId)?.name ?? '-';
|
||||
}
|
||||
if (presentation.value.mapTarget === 'nation' && nationTargetField.value) {
|
||||
const nationId = values[nationTargetField.value.key];
|
||||
if (typeof nationId !== 'number') return '-';
|
||||
return props.mapData?.nationList.find((nation) => nation[0] === nationId)?.[1] ?? '-';
|
||||
}
|
||||
return '-';
|
||||
});
|
||||
|
||||
const distanceFromMyCity = (destination: number): number | null => {
|
||||
const start = props.mapData?.myCity;
|
||||
if (!start || !props.mapLayout) return null;
|
||||
@@ -260,9 +280,23 @@ watch(
|
||||
:selected-city-id="mapSelectedCityId"
|
||||
:detail-mode="true"
|
||||
:fit-container="true"
|
||||
:show-current-city-marker="true"
|
||||
@select-city="selectMapCity"
|
||||
/>
|
||||
<small>지도에서 도시를 클릭하거나 아래 목록에서 대상을 선택하세요.</small>
|
||||
<div class="map-selection-status" aria-live="polite" data-testid="command-map-selection-status">
|
||||
<span class="current-city-status">
|
||||
<span class="status-key">현재 도시</span>
|
||||
<strong>{{ currentCityName }}</strong>
|
||||
</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span class="selected-target-status">
|
||||
<span class="status-key">{{
|
||||
presentation.mapTarget === 'nation' ? '선택 국가' : '선택 도시'
|
||||
}}</span>
|
||||
<strong>{{ selectedMapTargetName }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="mapTargetSummary" class="map-target-summary" data-testid="command-map-target-summary">
|
||||
{{ mapTargetSummary }}
|
||||
</div>
|
||||
@@ -376,6 +410,41 @@ watch(
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.map-selection-status {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 7px;
|
||||
padding: 0 8px 5px;
|
||||
color: rgba(232, 221, 196, 0.82);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.map-selection-status > span:not([aria-hidden='true']) {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-key {
|
||||
border-radius: 2px;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.current-city-status .status-key {
|
||||
border: 1px solid #82cfff;
|
||||
background: rgba(5, 27, 43, 0.92);
|
||||
color: #d9f3ff;
|
||||
}
|
||||
|
||||
.selected-target-status .status-key {
|
||||
border: 1px solid rgba(255, 235, 150, 0.9);
|
||||
background: rgba(201, 164, 90, 0.18);
|
||||
color: #ffe996;
|
||||
}
|
||||
|
||||
.command-guidance {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
|
||||
@@ -71,6 +71,7 @@ const props = withDefaults(
|
||||
selectedCityId?: number | null;
|
||||
detailMode?: boolean;
|
||||
fitContainer?: boolean;
|
||||
showCurrentCityMarker?: boolean;
|
||||
}>(),
|
||||
{
|
||||
// Vue casts an absent Boolean prop to false unless undefined is an explicit default.
|
||||
@@ -214,6 +215,20 @@ const cityViews = computed<CityView[]>(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const currentCityMarker = computed(() =>
|
||||
props.showCurrentCityMarker ? (cityViews.value.find((city) => city.isMyCity) ?? null) : null
|
||||
);
|
||||
|
||||
const currentCityMarkerStyle = computed(() => {
|
||||
const city = currentCityMarker.value;
|
||||
if (!city) return undefined;
|
||||
const verticalOffset = (effectiveDetailMode.value ? 22 : 14) * mapScale.value;
|
||||
return {
|
||||
left: `${city.x}px`,
|
||||
top: `${city.y - verticalOffset}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const mapSeason = computed(() => {
|
||||
if (!props.mapData) {
|
||||
return 'spring';
|
||||
@@ -433,6 +448,16 @@ const selectCity = (cityId: number) => {
|
||||
@leave="setHoveredCity(null)"
|
||||
@select="selectCity"
|
||||
/>
|
||||
<div
|
||||
v-if="currentCityMarker"
|
||||
class="current-city-marker"
|
||||
:style="currentCityMarkerStyle"
|
||||
data-testid="current-city-marker"
|
||||
role="note"
|
||||
:aria-label="`현재 도시 ${currentCityMarker.name}`"
|
||||
>
|
||||
현재
|
||||
</div>
|
||||
<div v-if="hoveredCity" class="map-tooltip" :style="tooltipPosition">
|
||||
<div class="tooltip-title">{{ hoveredCityTitle }}</div>
|
||||
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
|
||||
@@ -583,6 +608,38 @@ const selectCity = (cityId: number) => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.current-city-marker {
|
||||
position: absolute;
|
||||
z-index: 12;
|
||||
box-sizing: border-box;
|
||||
min-width: 30px;
|
||||
border: 1px solid #82cfff;
|
||||
border-radius: 2px;
|
||||
padding: 1px 4px;
|
||||
background: rgba(5, 27, 43, 0.92);
|
||||
color: #d9f3ff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px #000;
|
||||
transform: translate(-50%, -100%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.current-city-marker::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 4px solid transparent;
|
||||
border-top-color: #82cfff;
|
||||
content: '';
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user