From 1b07c5ffe9d294356ec34850b4b8ba7253eb24f9 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 13 Sep 2026 12:30:56 +0000 Subject: [PATCH] =?UTF-8?q?=EB=82=A8=EC=9D=80=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20NPC=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EC=A0=91=EB=91=90=EC=82=AC=EA=B0=80=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/commandArguments.spec.ts | 51 ++++++++++++++++++- app/game-frontend/e2e/mainNavigation.spec.ts | 49 +++++++++++++++++- .../e2e/nationCityOfficeIntegration.spec.ts | 32 ++++++++++-- .../src/components/chief/ChiefTurnCard.vue | 2 +- .../src/components/main/NationBasicCard.vue | 2 +- .../src/views/NationCitiesView.vue | 4 +- 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index a0404c9b..d9c410a2 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -847,7 +847,8 @@ const install = async ( clockRunning: boolean; clockRecovery: { startsAt: string; endsAt: string } | null; turnEngineRunning: boolean; - } + }, + decoratedNpcChiefs = false ) => { const requests: unknown[] = []; const currentGeneralContext = { @@ -982,7 +983,18 @@ const install = async ( turnTime: '2026-09-10T01:20:00Z', })), } - : chiefCenter + : decoratedNpcChiefs + ? { + ...chiefCenter, + chiefs: chiefCenter.chiefs.map((chief, index) => ({ + ...chief, + name: ['ⓖ의병', 'ⓤ중립', 'ⓞ특수', 'ⓧ기타', '㉥부대장', 'ⓜ인재', 'ⓝ장수', '사용자'][ + index + ]!, + npcState: [4, 6, 9, 7, 5, 3, 2, 0][index]!, + })), + } + : chiefCenter ); if (name === 'turns.reserved.getGeneral') return response({ turns: generalTurns, revision: generalRevision, autorunLimit: 2403 }); @@ -3554,3 +3566,38 @@ for (const width of [1200, 500]) { await expect(clock).toBeDisabled(); }); } + +test('NPC prefixes stay single in chief command cards', async ({ page }, testInfo) => { + await install(page, false, commandTable, 1, undefined, true); + for (const width of [1200, 500]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto(gamePath('/chief-center')); + const names = page.locator('.chief-card .chief-name, .chief-card .compact-name'); + // Desktop uses the editor for the current officer; mobile includes that officer in the overview. + await expect(names).toHaveText([ + 'ⓖ의병', + 'ⓤ중립', + 'ⓞ특수', + 'ⓧ기타', + '㉥부대장', + 'ⓜ인재', + 'ⓝ장수', + ...(width === 500 ? ['사용자'] : []), + ]); + await page.evaluate(() => document.fonts.ready); + await testInfo.attach(`npc-prefix-dom-${width}`, { + contentType: 'application/json', + body: JSON.stringify( + await page.locator('.chief-card').evaluateAll((elements) => + elements.map((element) => ({ + html: element.outerHTML, + rect: element.getBoundingClientRect().toJSON(), + fontSize: getComputedStyle(element).fontSize, + color: getComputedStyle(element).color, + })) + ) + ), + }); + await page.screenshot({ path: testInfo.outputPath(`npc-chief-prefix-${width}.png`), fullPage: true }); + } +}); diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 4df9f181..ffc350d0 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -29,6 +29,7 @@ const operationNames = (route: Route) => decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(','); type NavigationFixture = { + decoratedNpcChiefs?: boolean; officerLevel: number; permission: number; nationLevel: number; @@ -546,8 +547,16 @@ const generalContext = (state: NavigationFixture) => ({ techLevel: 0, techLimited: false, topChiefs: { - 12: { id: 1, name: '군주', npcState: 0 }, - 11: { id: 2, name: '참모', npcState: 1 }, + 12: { + id: 1, + name: state.decoratedNpcChiefs ? 'ⓖ의병군주' : '군주', + npcState: state.decoratedNpcChiefs ? 4 : 0, + }, + 11: { + id: 2, + name: state.decoratedNpcChiefs ? 'ⓤ중립참모' : '참모', + npcState: state.decoratedNpcChiefs ? 6 : 1, + }, }, impossibleStrategicCommands: [{ name: '수몰', remainingTurns: 2, availableYear: 190, availableMonth: 5 }], }, @@ -6836,3 +6845,39 @@ test('main nation and general cards fill their panels without increasing overflo ); } }); + +test('NPC prefixes stay single in the main nation card', async ({ page }, testInfo) => { + await installFixture(page, { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 0, + npcMode: 1, + generalMeCalls: 0, + operations: [], + decoratedNpcChiefs: true, + validMapImages: true, + }); + for (const width of [1200, 500]) { + await page.setViewportSize({ width, height: 900 }); + await waitForMain(page); + const card = page.locator('[data-nation-basic-card]'); + await expect(card.getByText('ⓖ의병군주', { exact: true })).toBeVisible(); + await expect(card.getByText('ⓤ중립참모', { exact: true })).toBeVisible(); + await page.evaluate(() => document.fonts.ready); + await testInfo.attach(`npc-prefix-dom-${width}`, { + contentType: 'application/json', + body: JSON.stringify( + await page.locator('[data-nation-basic-card]').evaluateAll((elements) => + elements.map((element) => ({ + html: element.outerHTML, + rect: element.getBoundingClientRect().toJSON(), + fontSize: getComputedStyle(element).fontSize, + color: getComputedStyle(element).color, + })) + ) + ), + }); + await card.screenshot({ path: testInfo.outputPath(`npc-main-prefix-${width}.png`) }); + } +}); diff --git a/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts b/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts index 9c3e8861..4d03ec05 100644 --- a/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts +++ b/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts @@ -160,8 +160,8 @@ const overviewFixture = (state: FixtureState) => ({ }, { id: 22, - name: '조홍', - npcState: 2, + name: 'ⓖ조홍', + npcState: 4, officerLevel: 1, cityId: 2, officerCity: 0, @@ -222,7 +222,7 @@ const secretFixture = () => ({ secretGeneral(21, '장료', 1, { stats: { leadership: 80, strength: 70, intelligence: 50 }, }), - secretGeneral(22, '조홍', 2, { npcState: 2, reservedCommands: [] }), + secretGeneral(22, 'ⓖ조홍', 2, { npcState: 4, reservedCommands: [] }), ], }); @@ -465,3 +465,29 @@ test('암행부 권한 거부는 도시 기밀 행과 인사부 연동을 열지 await expect(page.getByRole('button', { name: '인사부 연동' })).toHaveCount(0); expect(state.appointmentInputs).toEqual([]); }); + +test('NPC prefixes stay single in city names and linked secret rows', async ({ page }, testInfo) => { + await install(page, { role: 'head', appointed: false, appointmentInputs: [] }); + for (const width of [1200, 500]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto('nation/cities'); + await expect(page.locator('.general-list').getByText('ⓖ조홍', { exact: true })).toHaveCount(1); + await page.getByRole('button', { name: '암행부 연동' }).click(); + await expect(page.locator('[data-general-id="22"] .secret-name-cell > span').first()).toHaveText('ⓖ조홍'); + await page.evaluate(() => document.fonts.ready); + await testInfo.attach(`npc-prefix-dom-${width}`, { + contentType: 'application/json', + body: JSON.stringify( + await page.locator('.nation-cities-page').evaluateAll((elements) => + elements.map((element) => ({ + html: element.outerHTML, + rect: element.getBoundingClientRect().toJSON(), + fontSize: getComputedStyle(element).fontSize, + color: getComputedStyle(element).color, + })) + ) + ), + }); + await page.screenshot({ path: testInfo.outputPath(`npc-city-prefix-${width}.png`), fullPage: true }); + } +}); diff --git a/app/game-frontend/src/components/chief/ChiefTurnCard.vue b/app/game-frontend/src/components/chief/ChiefTurnCard.vue index 415d463c..351791f9 100644 --- a/app/game-frontend/src/components/chief/ChiefTurnCard.vue +++ b/app/game-frontend/src/components/chief/ChiefTurnCard.vue @@ -28,7 +28,7 @@ const emit = defineEmits<{ const nameColor = computed(() => (props.npcState !== null ? getNpcColor(props.npcState) : undefined)); const displayName = computed(() => { const name = props.name ?? '-'; - return (props.npcState ?? 0) > 0 && !/^[ⓜⓝ]/u.test(name) ? `ⓝ${name}` : name; + return (props.npcState ?? 0) > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(name) ? `ⓝ${name}` : name; }); const handleClick = () => { diff --git a/app/game-frontend/src/components/main/NationBasicCard.vue b/app/game-frontend/src/components/main/NationBasicCard.vue index 8435b3b5..0cb01852 100644 --- a/app/game-frontend/src/components/main/NationBasicCard.vue +++ b/app/game-frontend/src/components/main/NationBasicCard.vue @@ -51,7 +51,7 @@ const props = defineProps<{ const number = (value: number): string => value.toLocaleString('ko-KR'); const displayChiefName = (chief: NationChief | undefined): string => { if (!chief) return '-'; - return chief.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(chief.name) ? `ⓝ${chief.name}` : chief.name; + return chief.npcState > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(chief.name) ? `ⓝ${chief.name}` : chief.name; }; diff --git a/app/game-frontend/src/views/NationCitiesView.vue b/app/game-frontend/src/views/NationCitiesView.vue index 2fb0e25e..c854d613 100644 --- a/app/game-frontend/src/views/NationCitiesView.vue +++ b/app/game-frontend/src/views/NationCitiesView.vue @@ -69,9 +69,9 @@ const generalsForCity = (cityId: number) => const secretGeneralsForCity = (cityId: number) => sortGeneralsByTypeThenName(secretData.value?.generals.filter((general) => general.cityId === cityId) ?? []); const displayGeneralName = (general: Result['generals'][number]) => - general.npcState > 0 && !/^[ⓜⓝ]/u.test(general.name) ? `ⓝ${general.name}` : general.name; + general.npcState > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(general.name) ? `ⓝ${general.name}` : general.name; const displaySecretGeneralName = (general: SecretGeneral) => - general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `ⓝ${general.name}` : general.name; + general.npcState > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(general.name) ? `ⓝ${general.name}` : general.name; const generalCount = (cityId: number) => data.value?.generals.filter((general) => general.cityId === cityId).length ?? 0; const cities = computed(() => {