feat: 메인 도시 정보를 Ref와 일치
도시 소유 국가색과 태수·군사·종사를 general.me에 추가한다. 주민 영역, 반응형 grid와 국가색 대비를 Ref 카드와 같은 geometry로 맞추고 API 및 production Chromium 회귀 검증을 추가한다.
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
resolveNationBlockWar,
|
resolveNationBlockWar,
|
||||||
resolveNationNotice,
|
resolveNationNotice,
|
||||||
resolveNationRate,
|
resolveNationRate,
|
||||||
|
resolveOfficerCity,
|
||||||
type TraitNameMap,
|
type TraitNameMap,
|
||||||
} from '../nation/shared.js';
|
} from '../nation/shared.js';
|
||||||
import {
|
import {
|
||||||
@@ -348,38 +349,73 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
]);
|
]);
|
||||||
const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;
|
const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;
|
||||||
|
|
||||||
const [capitalCity, cityNation, troopLeaderCity, nationPopulation, nationCrew, topChiefRows] = await Promise.all([
|
const [capitalCity, cityNation, troopLeaderCity, nationPopulation, nationCrew, chiefAndCityOfficerRows] =
|
||||||
nation.capitalCityId
|
await Promise.all([
|
||||||
? ctx.db.city.findUnique({ where: { id: nation.capitalCityId }, select: { name: true } })
|
nation.capitalCityId
|
||||||
: Promise.resolve(null),
|
? ctx.db.city.findUnique({ where: { id: nation.capitalCityId }, select: { name: true } })
|
||||||
city && city.nationId > 0
|
: Promise.resolve(null),
|
||||||
? ctx.db.nation.findUnique({ where: { id: city.nationId }, select: { name: true } })
|
city && city.nationId > 0
|
||||||
: Promise.resolve(null),
|
? ctx.db.nation.findUnique({ where: { id: city.nationId }, select: { name: true, color: true } })
|
||||||
troopLeader && troopLeader.cityId > 0
|
: Promise.resolve(null),
|
||||||
? ctx.db.city.findUnique({ where: { id: troopLeader.cityId }, select: { name: true } })
|
troopLeader && troopLeader.cityId > 0
|
||||||
: Promise.resolve(null),
|
? ctx.db.city.findUnique({ where: { id: troopLeader.cityId }, select: { name: true } })
|
||||||
nation.id > 0
|
: Promise.resolve(null),
|
||||||
? ctx.db.city.aggregate({
|
nation.id > 0
|
||||||
where: { nationId: nation.id },
|
? ctx.db.city.aggregate({
|
||||||
_count: true,
|
where: { nationId: nation.id },
|
||||||
_sum: { population: true, populationMax: true },
|
_count: true,
|
||||||
})
|
_sum: { population: true, populationMax: true },
|
||||||
: Promise.resolve({ _count: 0, _sum: { population: 0, populationMax: 0 } }),
|
})
|
||||||
nation.id > 0
|
: Promise.resolve({ _count: 0, _sum: { population: 0, populationMax: 0 } }),
|
||||||
? ctx.db.general.aggregate({
|
nation.id > 0
|
||||||
where: { nationId: nation.id, npcState: { not: 5 } },
|
? ctx.db.general.aggregate({
|
||||||
_count: true,
|
where: { nationId: nation.id, npcState: { not: 5 } },
|
||||||
_sum: { crew: true, leadership: true },
|
_count: true,
|
||||||
})
|
_sum: { crew: true, leadership: true },
|
||||||
: Promise.resolve({ _count: 0, _sum: { crew: 0, leadership: 0 } }),
|
})
|
||||||
nation.id > 0
|
: Promise.resolve({ _count: 0, _sum: { crew: 0, leadership: 0 } }),
|
||||||
? ctx.db.general.findMany({
|
nation.id > 0 || city
|
||||||
where: { nationId: nation.id, officerLevel: { gte: 11 } },
|
? ctx.db.general.findMany({
|
||||||
select: { id: true, name: true, npcState: true, officerLevel: true },
|
where: {
|
||||||
orderBy: { id: 'asc' },
|
OR: [
|
||||||
})
|
{ nationId: nation.id > 0 ? nation.id : -1, officerLevel: { gte: 11 } },
|
||||||
: Promise.resolve([]),
|
{
|
||||||
]);
|
officerLevel: { in: [2, 3, 4] },
|
||||||
|
meta: { path: ['officerCity'], equals: city?.id ?? -1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
officerLevel: { in: [2, 3, 4] },
|
||||||
|
meta: { path: ['officer_city'], equals: city?.id ?? -1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: { id: true, name: true, npcState: true, nationId: true, officerLevel: true, meta: true },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
})
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
const topChiefRows = chiefAndCityOfficerRows.filter(
|
||||||
|
(chief) => nation.id > 0 && chief.nationId === nation.id && chief.officerLevel >= 11
|
||||||
|
);
|
||||||
|
const cityOfficers: Record<2 | 3 | 4, { id: number; name: string; npcState: number } | null> = {
|
||||||
|
2: null,
|
||||||
|
3: null,
|
||||||
|
4: null,
|
||||||
|
};
|
||||||
|
if (city) {
|
||||||
|
for (const officer of chiefAndCityOfficerRows) {
|
||||||
|
if (
|
||||||
|
(officer.officerLevel === 2 || officer.officerLevel === 3 || officer.officerLevel === 4) &&
|
||||||
|
resolveOfficerCity(asRecord(officer.meta)) === city.id
|
||||||
|
) {
|
||||||
|
cityOfficers[officer.officerLevel] = {
|
||||||
|
id: officer.id,
|
||||||
|
name: officer.name,
|
||||||
|
npcState: officer.npcState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const [personalityNames, domesticNames, warNames, nationTypeNames, crewTypeNames, itemNames] = await Promise.all([
|
const [personalityNames, domesticNames, warNames, nationTypeNames, crewTypeNames, itemNames] = await Promise.all([
|
||||||
loadTraitNames([general.personalCode], 'personality'),
|
loadTraitNames([general.personalCode], 'personality'),
|
||||||
loadTraitNames([general.specialCode], 'domestic'),
|
loadTraitNames([general.specialCode], 'domestic'),
|
||||||
@@ -528,6 +564,8 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
|||||||
levelName: resolveCityLevelName(city.level),
|
levelName: resolveCityLevelName(city.level),
|
||||||
regionName: resolveRegionName(city.region),
|
regionName: resolveRegionName(city.region),
|
||||||
nationName: city.nationId > 0 ? (cityNation?.name ?? '-') : '공백지',
|
nationName: city.nationId > 0 ? (cityNation?.name ?? '-') : '공백지',
|
||||||
|
nationColor: city.nationId > 0 ? (cityNation?.color ?? '#000000') : '#000000',
|
||||||
|
officers: cityOfficers,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
nation: {
|
nation: {
|
||||||
|
|||||||
@@ -290,6 +290,64 @@ describe('in-game my information ownership', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns the current city nation color and its Ref city officers', async () => {
|
||||||
|
const me = buildGeneral();
|
||||||
|
const fixture = createContext({
|
||||||
|
me,
|
||||||
|
targets: [
|
||||||
|
me,
|
||||||
|
buildGeneral({ id: 8, name: '태수장', officerLevel: 4, npcState: 0, meta: { officerCity: 1 } }),
|
||||||
|
buildGeneral({ id: 9, name: '군사장', officerLevel: 3, npcState: 2, meta: { officer_city: 1 } }),
|
||||||
|
buildGeneral({ id: 10, name: '타도시종사', officerLevel: 2, npcState: 6, meta: { officerCity: 2 } }),
|
||||||
|
],
|
||||||
|
city: {
|
||||||
|
id: 1,
|
||||||
|
name: '업',
|
||||||
|
level: 8,
|
||||||
|
nationId: 1,
|
||||||
|
population: 1_000,
|
||||||
|
populationMax: 2_000,
|
||||||
|
agriculture: 100,
|
||||||
|
agricultureMax: 200,
|
||||||
|
commerce: 100,
|
||||||
|
commerceMax: 200,
|
||||||
|
security: 100,
|
||||||
|
securityMax: 200,
|
||||||
|
trust: 70,
|
||||||
|
trade: 100,
|
||||||
|
defence: 100,
|
||||||
|
defenceMax: 200,
|
||||||
|
wall: 100,
|
||||||
|
wallMax: 200,
|
||||||
|
region: 2,
|
||||||
|
supplyState: 1,
|
||||||
|
frontState: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||||
|
city: {
|
||||||
|
nationName: '위',
|
||||||
|
nationColor: '#777777',
|
||||||
|
officers: {
|
||||||
|
4: { id: 8, name: '태수장', npcState: 0 },
|
||||||
|
3: { id: 9, name: '군사장', npcState: 2 },
|
||||||
|
2: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(fixture.db.general.findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: {
|
||||||
|
OR: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ meta: { path: ['officerCity'], equals: 1 } }),
|
||||||
|
expect.objectContaining({ meta: { path: ['officer_city'], equals: 1 } }),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns Ref display names instead of numeric levels and internal codes for the main GUI', async () => {
|
it('returns Ref display names instead of numeric levels and internal codes for the main GUI', async () => {
|
||||||
const fixture = createContext({
|
const fixture = createContext({
|
||||||
me: buildGeneral({
|
me: buildGeneral({
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ const myGeneral = (state: FixtureState) => ({
|
|||||||
regionName: '중원',
|
regionName: '중원',
|
||||||
nationId: 1,
|
nationId: 1,
|
||||||
nationName: '위',
|
nationName: '위',
|
||||||
|
nationColor: '#ffff00',
|
||||||
population: 1000,
|
population: 1000,
|
||||||
populationMax: 2000,
|
populationMax: 2000,
|
||||||
agriculture: 100,
|
agriculture: 100,
|
||||||
@@ -139,6 +140,11 @@ const myGeneral = (state: FixtureState) => ({
|
|||||||
wallMax: 200,
|
wallMax: 200,
|
||||||
supplyState: 1,
|
supplyState: 1,
|
||||||
frontState: 0,
|
frontState: 0,
|
||||||
|
officers: {
|
||||||
|
4: { id: 8, name: '태수장', npcState: 0 },
|
||||||
|
3: { id: 9, name: '군사장', npcState: 2 },
|
||||||
|
2: { id: 10, name: '종사장', npcState: 6 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
nation: state.buildNationCandidateEnabled
|
nation: state.buildNationCandidateEnabled
|
||||||
? {
|
? {
|
||||||
@@ -679,11 +685,76 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명
|
|||||||
await expect(generalCard).toContainText('계급 29품관');
|
await expect(generalCard).toContainText('계급 29품관');
|
||||||
|
|
||||||
const cityCard = page.locator('.city-card');
|
const cityCard = page.locator('.city-card');
|
||||||
await expect(cityCard.locator('.title')).toContainText('【중원 | 특】 업');
|
await expect(cityCard.locator('.city-title')).toHaveText('【중원 | 특】 업');
|
||||||
await expect(cityCard.locator('.title')).toContainText('지배 국가 【 위 】');
|
await expect(cityCard.locator('.city-nation')).toHaveText('지배 국가 【 위 】');
|
||||||
await expect(page.locator('.main-page')).not.toContainText('che_');
|
await expect(page.locator('.main-page')).not.toContainText('che_');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const viewport of [
|
||||||
|
{ name: 'desktop', width: 1000, height: 900, columns: 4, cardWidth: 700, cardHeight: 125.390625 },
|
||||||
|
{ name: 'mobile', width: 500, height: 900, columns: 3, cardWidth: 500, cardHeight: 147.390625 },
|
||||||
|
] as const) {
|
||||||
|
test(`메인 도시 카드는 Ref의 국가색·주민 폭·도시 관직 배치를 유지한다 (${viewport.name})`, async ({ page }) => {
|
||||||
|
const state: FixtureState = {
|
||||||
|
permission: 'head',
|
||||||
|
myset: 3,
|
||||||
|
settingMutations: [],
|
||||||
|
accessPages: [],
|
||||||
|
};
|
||||||
|
await install(page, state);
|
||||||
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||||
|
await page.goto('');
|
||||||
|
|
||||||
|
const cityCard = page.locator('[data-city-basic-card]');
|
||||||
|
await expect(cityCard.locator('.city-title')).toHaveText('【중원 | 특】 업');
|
||||||
|
await expect(cityCard.locator('.city-nation')).toHaveText('지배 국가 【 위 】');
|
||||||
|
await expect(cityCard.locator('[data-city-officer="4"]')).toContainText('태수장');
|
||||||
|
await expect(cityCard.locator('[data-city-officer="3"]')).toContainText('군사장');
|
||||||
|
await expect(cityCard.locator('[data-city-officer="2"]')).toContainText('종사장');
|
||||||
|
|
||||||
|
const geometry = await cityCard.evaluate((element) => {
|
||||||
|
const rect = (selector: string) => element.querySelector<HTMLElement>(selector)!.getBoundingClientRect();
|
||||||
|
const style = (selector: string) => getComputedStyle(element.querySelector<HTMLElement>(selector)!);
|
||||||
|
const cardRect = element.getBoundingClientRect();
|
||||||
|
const populationRect = rect('[data-city-progress="주민"]');
|
||||||
|
const trustRect = rect('[data-city-progress="민심"]');
|
||||||
|
const populationBarRect = rect('[data-city-progress="주민"] [role="progressbar"]');
|
||||||
|
const trustBarRect = rect('[data-city-progress="민심"] [role="progressbar"]');
|
||||||
|
return {
|
||||||
|
card: { width: cardRect.width, height: cardRect.height },
|
||||||
|
columns: style('.city-grid').gridTemplateColumns.split(' ').length,
|
||||||
|
title: {
|
||||||
|
backgroundColor: style('.city-title').backgroundColor,
|
||||||
|
color: style('.city-title').color,
|
||||||
|
},
|
||||||
|
nation: {
|
||||||
|
backgroundColor: style('.city-nation').backgroundColor,
|
||||||
|
color: style('.city-nation').color,
|
||||||
|
},
|
||||||
|
populationWidth: populationRect.width,
|
||||||
|
trustWidth: trustRect.width,
|
||||||
|
populationBarWidth: populationBarRect.width,
|
||||||
|
trustBarWidth: trustBarRect.width,
|
||||||
|
officerColors: [4, 3, 2].map(
|
||||||
|
(level) => style(`[data-city-officer="${level}"] .city-officer__name`).color
|
||||||
|
),
|
||||||
|
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(geometry.columns).toBe(viewport.columns);
|
||||||
|
expect(geometry.card.width).toBeCloseTo(viewport.cardWidth, 0);
|
||||||
|
expect(geometry.card.height).toBeCloseTo(viewport.cardHeight, 0);
|
||||||
|
expect(geometry.title).toEqual({ backgroundColor: 'rgb(255, 255, 0)', color: 'rgb(0, 0, 0)' });
|
||||||
|
expect(geometry.nation).toEqual({ backgroundColor: 'rgb(255, 255, 0)', color: 'rgb(0, 0, 0)' });
|
||||||
|
expect(geometry.populationWidth).toBeCloseTo(geometry.trustWidth * 2, 0);
|
||||||
|
expect(geometry.populationBarWidth).toBeGreaterThan(geometry.trustBarWidth * 2.4);
|
||||||
|
expect(geometry.officerColors).toEqual(['rgb(255, 255, 255)', 'rgb(0, 255, 255)', 'rgb(102, 205, 170)']);
|
||||||
|
expect(geometry.overflow).toBe(0);
|
||||||
|
await persistParityArtifact(page, `main-city-card-${viewport.name}`, geometry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('메인 장수 동향과 개인 전투 기록은 Ref 행 간격·색상·글자 크기를 유지한다', async ({ page }) => {
|
test('메인 장수 동향과 개인 전투 기록은 Ref 행 간격·색상·글자 크기를 유지한다', async ({ page }) => {
|
||||||
const state: FixtureState = {
|
const state: FixtureState = {
|
||||||
permission: 'head',
|
permission: 'head',
|
||||||
|
|||||||
@@ -419,8 +419,12 @@ const generalContext = (state: NavigationFixture) => ({
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: '업',
|
name: '업',
|
||||||
level: 8,
|
level: 8,
|
||||||
|
levelName: '특',
|
||||||
nationId: 1,
|
nationId: 1,
|
||||||
|
nationName: '위',
|
||||||
|
nationColor: state.nationColor ?? '#008000',
|
||||||
region: 1,
|
region: 1,
|
||||||
|
regionName: '중원',
|
||||||
population: 100_000,
|
population: 100_000,
|
||||||
populationMax: 200_000,
|
populationMax: 200_000,
|
||||||
agriculture: 1_000,
|
agriculture: 1_000,
|
||||||
@@ -438,6 +442,11 @@ const generalContext = (state: NavigationFixture) => ({
|
|||||||
supplyState: 1,
|
supplyState: 1,
|
||||||
frontState: 0,
|
frontState: 0,
|
||||||
state: state.cityState ?? 0,
|
state: state.cityState ?? 0,
|
||||||
|
officers: {
|
||||||
|
4: { id: 8, name: '태수장', npcState: 0 },
|
||||||
|
3: { id: 9, name: '군사장', npcState: 2 },
|
||||||
|
2: { id: 10, name: '종사장', npcState: 6 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
nation: {
|
nation: {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import { computed } from 'vue';
|
|||||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||||
import LegacyProgressBar from '../ui/LegacyProgressBar.vue';
|
import LegacyProgressBar from '../ui/LegacyProgressBar.vue';
|
||||||
import { ratioPercent } from '../../utils/legacyProgress';
|
import { ratioPercent } from '../../utils/legacyProgress';
|
||||||
|
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
|
||||||
|
import { getNpcColor } from '../../utils/npcColor';
|
||||||
|
|
||||||
|
interface CityOfficer {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
npcState: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface CityInfo {
|
interface CityInfo {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -12,6 +20,7 @@ interface CityInfo {
|
|||||||
regionName: string;
|
regionName: string;
|
||||||
nationId: number;
|
nationId: number;
|
||||||
nationName: string;
|
nationName: string;
|
||||||
|
nationColor: string;
|
||||||
population: number;
|
population: number;
|
||||||
populationMax: number;
|
populationMax: number;
|
||||||
agriculture: number;
|
agriculture: number;
|
||||||
@@ -28,6 +37,7 @@ interface CityInfo {
|
|||||||
wallMax: number;
|
wallMax: number;
|
||||||
supplyState: number;
|
supplyState: number;
|
||||||
frontState: number;
|
frontState: number;
|
||||||
|
officers?: Partial<Record<2 | 3 | 4, CityOfficer | null>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -59,97 +69,170 @@ const metrics = computed(() => {
|
|||||||
{ label: '시세', percent: city.trade ? ratioPercent(city.trade - 95, 10) : 0, text: tradeText },
|
{ label: '시세', percent: city.trade ? ratioPercent(city.trade - 95, 10) : 0, text: tradeText },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const officerRows = computed(() => {
|
||||||
|
const city = props.city;
|
||||||
|
if (!city) return [];
|
||||||
|
return [
|
||||||
|
{ level: 4 as const, label: '태수', officer: city.officers?.[4] },
|
||||||
|
{ level: 3 as const, label: '군사', officer: city.officers?.[3] },
|
||||||
|
{ level: 2 as const, label: '종사', officer: city.officers?.[2] },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const nationStyle = computed(() => {
|
||||||
|
const backgroundColor = props.city?.nationColor ?? '#000000';
|
||||||
|
return {
|
||||||
|
backgroundColor,
|
||||||
|
color: legacyLuminanceTextColor(backgroundColor),
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="city-card">
|
<div class="city-card" data-city-basic-card>
|
||||||
<div v-if="props.loading">
|
<div v-if="props.loading">
|
||||||
<SkeletonLines :lines="4" />
|
<SkeletonLines :lines="4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!props.city" class="empty">도시 정보를 불러오지 못했습니다.</div>
|
<div v-else-if="!props.city" class="empty">도시 정보를 불러오지 못했습니다.</div>
|
||||||
<div v-else class="city-body">
|
<div v-else class="city-grid">
|
||||||
<div class="title">
|
<div class="city-title" :style="nationStyle">
|
||||||
【{{ props.city.regionName }} | {{ props.city.levelName }}】 {{ props.city.name }} ·
|
【{{ props.city.regionName }} | {{ props.city.levelName }}】 {{ props.city.name }}
|
||||||
|
</div>
|
||||||
|
<div class="city-nation" :style="nationStyle">
|
||||||
{{ props.city.nationId > 0 ? `지배 국가 【 ${props.city.nationName} 】` : '공 백 지' }}
|
{{ props.city.nationId > 0 ? `지배 국가 【 ${props.city.nationName} 】` : '공 백 지' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-grid">
|
<div
|
||||||
<div
|
v-for="metric of metrics"
|
||||||
v-for="metric of metrics"
|
:key="metric.label"
|
||||||
:key="metric.label"
|
class="city-panel city-progress"
|
||||||
class="city-progress"
|
:class="{ 'city-panel--population': metric.label === '주민' }"
|
||||||
:data-city-progress="metric.label"
|
:data-city-progress="metric.label"
|
||||||
>
|
>
|
||||||
<span class="city-progress__label">{{ metric.label }}</span>
|
<span class="city-panel__head city-progress__label">{{ metric.label }}</span>
|
||||||
<div class="city-progress__body">
|
<div class="city-panel__body city-progress__body">
|
||||||
<LegacyProgressBar
|
<LegacyProgressBar
|
||||||
:height="7"
|
:height="7"
|
||||||
:percent="metric.percent"
|
:percent="metric.percent"
|
||||||
:label="`${metric.label} ${metric.text}`"
|
:label="`${metric.label} ${metric.text}`"
|
||||||
/>
|
/>
|
||||||
<span class="city-progress__text">{{ metric.text }}</span>
|
<span class="city-progress__text">{{ metric.text }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="row of officerRows"
|
||||||
|
:key="row.level"
|
||||||
|
class="city-panel city-officer"
|
||||||
|
:class="`city-panel--officer-${row.level}`"
|
||||||
|
:data-city-officer="row.level"
|
||||||
|
>
|
||||||
|
<span class="city-panel__head">{{ row.label }}</span>
|
||||||
|
<span
|
||||||
|
class="city-panel__body city-officer__name"
|
||||||
|
:style="{ color: getNpcColor(row.officer?.npcState ?? 0) }"
|
||||||
|
>
|
||||||
|
{{ row.officer?.name ?? '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.title {
|
.city-card,
|
||||||
|
.city-grid {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 20px;
|
width: 100%;
|
||||||
padding: 1px 6px;
|
|
||||||
border-bottom: 1px solid #666;
|
|
||||||
background: #173d27;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.city-progress {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 31px;
|
|
||||||
border-right: 1px solid #666;
|
|
||||||
border-bottom: 1px solid #666;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.city-progress__label {
|
.city-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
background-color: #172a52;
|
||||||
|
background-image: var(--sammo-texture-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-title,
|
||||||
|
.city-nation {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-width: 0;
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
border-left: 1px solid gray;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-panel {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
border-left: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-panel--population {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-panel__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgb(20 75 42 / 70%);
|
background: rgb(20 75 42 / 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.city-panel__body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.city-progress__body {
|
.city-progress__body {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
|
||||||
align-content: center;
|
align-content: center;
|
||||||
padding: 1px 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.city-progress__text {
|
.city-progress__text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 14px;
|
line-height: 1.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.city-officer__name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: rgba(232, 221, 196, 0.6);
|
color: rgba(232, 221, 196, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 939.98px) {
|
@media (min-width: 940px) {
|
||||||
.progress-grid {
|
.city-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-panel--officer-4 {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
grid-row: 3 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-panel--officer-3 {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
grid-row: 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city-panel--officer-2 {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
grid-row: 5 / 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -756,7 +756,12 @@ button {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-main-target='map'] :deep(.panel-body) {
|
[data-main-target='city'] :deep(.panel-header) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-main-target='map'] :deep(.panel-body),
|
||||||
|
[data-main-target='city'] :deep(.panel-body) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ try {
|
|||||||
`reference main progress bars missing: ${JSON.stringify({
|
`reference main progress bars missing: ${JSON.stringify({
|
||||||
url: page.url(),
|
url: page.url(),
|
||||||
title: await page.title(),
|
title: await page.title(),
|
||||||
|
bodyText: (await page.locator('body').innerText()).slice(0, 500),
|
||||||
cityCards: await page.locator('.city-card-basic').count(),
|
cityCards: await page.locator('.city-card-basic').count(),
|
||||||
generalCards: await page.locator('.general-card-basic').count(),
|
generalCards: await page.locator('.general-card-basic').count(),
|
||||||
legacyBars: await page.locator('.bar_out').count(),
|
legacyBars: await page.locator('.bar_out').count(),
|
||||||
@@ -84,11 +85,33 @@ try {
|
|||||||
fillBackgroundImage: fillStyle?.backgroundImage ?? '',
|
fillBackgroundImage: fillStyle?.backgroundImage ?? '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const inspectPanel = (selector) => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (!(element instanceof HTMLElement)) return null;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
text: element.textContent?.replace(/\s+/gu, ' ').trim() ?? '',
|
||||||
|
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
||||||
|
backgroundColor: style.backgroundColor,
|
||||||
|
color: style.color,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const cityCard = document.querySelector('.city-card-basic');
|
||||||
|
if (!(cityCard instanceof HTMLElement)) throw new Error('reference city card missing');
|
||||||
return {
|
return {
|
||||||
viewport: { width: innerWidth, height: innerHeight },
|
viewport: { width: innerWidth, height: innerHeight },
|
||||||
city: [...document.querySelectorAll('.city-card-basic .sammo-bar')].map(inspect),
|
city: [...document.querySelectorAll('.city-card-basic .sammo-bar')].map(inspect),
|
||||||
general: [...document.querySelectorAll('.general-card-basic .sammo-bar')].map(inspect),
|
general: [...document.querySelectorAll('.general-card-basic .sammo-bar')].map(inspect),
|
||||||
cityCard: document.querySelector('.city-card-basic').getBoundingClientRect().toJSON(),
|
cityCard: cityCard.getBoundingClientRect().toJSON(),
|
||||||
|
cityLayout: {
|
||||||
|
gridTemplateColumns: getComputedStyle(cityCard).gridTemplateColumns,
|
||||||
|
title: inspectPanel('.city-card-basic .cityNamePanel'),
|
||||||
|
nation: inspectPanel('.city-card-basic .nationNamePanel'),
|
||||||
|
population: inspectPanel('.city-card-basic .popPanel'),
|
||||||
|
trust: inspectPanel('.city-card-basic .trustPanel'),
|
||||||
|
officers: [4, 3, 2].map((level) => inspectPanel(`.city-card-basic .officer${level}Panel`)),
|
||||||
|
},
|
||||||
generalCard: document.querySelector('.general-card-basic').getBoundingClientRect().toJSON(),
|
generalCard: document.querySelector('.general-card-basic').getBoundingClientRect().toJSON(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user