diff --git a/app/game-frontend/e2e/inGameInfo.spec.ts b/app/game-frontend/e2e/inGameInfo.spec.ts index 958fefbe..4715b3d6 100644 --- a/app/game-frontend/e2e/inGameInfo.spec.ts +++ b/app/game-frontend/e2e/inGameInfo.spec.ts @@ -303,7 +303,7 @@ const install = async ( { id: 2, name: '적국', - color: '#800000', + color: '#FFFF00', capitalCityId: 2, level: 1, power: 1000, @@ -594,6 +594,14 @@ test('global-info renders the ref nation summary columns beside the map', async await expect(summary.locator('thead')).toContainText('속령'); await expect(summary.locator('tbody tr').first()).toHaveText(/아국\s*1,234\s*2\s*1/u); await expect(summary.locator('tbody tr').first().locator('td').last()).toHaveAttribute('title', '업'); + await expect(summary.locator('tbody tr').first().locator('td').first().locator('span')).toHaveCSS( + 'color', + 'rgb(255, 255, 255)' + ); + await expect(summary.locator('tbody tr').nth(1).locator('td').first().locator('span')).toHaveCSS( + 'color', + 'rgb(0, 0, 0)' + ); const geometry = await summary.evaluate((element) => { const rect = element.getBoundingClientRect(); diff --git a/app/game-frontend/e2e/joinLayout.spec.ts b/app/game-frontend/e2e/joinLayout.spec.ts index 0df9167b..e7d8615c 100644 --- a/app/game-frontend/e2e/joinLayout.spec.ts +++ b/app/game-frontend/e2e/joinLayout.spec.ts @@ -56,7 +56,7 @@ const installFixture = async (page: Page, state: FixtureState): Promise => ], warSpecials: [{ key: 'che_무쌍', name: '무쌍', info: '전투 특기' }], nations: [ - { id: 1, name: '촉', color: '#66aa44', scoutMessage: '함께 천하를 도모합시다.' }, + { id: 1, name: '촉', color: '#FFFF00', scoutMessage: '함께 천하를 도모합시다.' }, { id: 2, name: '위', color: '#5577bb', scoutMessage: '능력 있는 장수를 기다립니다.' }, ], serverInfo: { @@ -164,6 +164,12 @@ test('prioritizes core general fields and keeps context and inheritance progress await page.setViewportSize({ width: 1200, height: 900 }); await page.goto('join'); + const nationHeadings = page.locator('.nation-card .nation-name'); + await expect(nationHeadings.nth(0)).toHaveCSS('background-color', 'rgb(255, 255, 0)'); + await expect(nationHeadings.nth(0)).toHaveCSS('color', 'rgb(0, 0, 0)'); + await expect(nationHeadings.nth(1)).toHaveCSS('background-color', 'rgb(85, 119, 187)'); + await expect(nationHeadings.nth(1)).toHaveCSS('color', 'rgb(255, 255, 255)'); + const flow = page.locator('.join-flow'); const basicPanel = flow.locator('.panel-card').first(); const advanced = flow.locator('.advanced-options'); @@ -347,6 +353,10 @@ test('keeps the primary creation flow readable without horizontal overflow on mo await page.setViewportSize({ width: 390, height: 844 }); await page.goto('join'); + const nationHeadings = page.locator('.nation-card .nation-name'); + await expect(nationHeadings.nth(0)).toHaveCSS('color', 'rgb(0, 0, 0)'); + await expect(nationHeadings.nth(1)).toHaveCSS('color', 'rgb(255, 255, 255)'); + await expect(page.getByRole('heading', { name: '장수 기본 정보' })).toBeVisible(); const mobileGeometry = await page.evaluate(() => { const flow = document.querySelector('.join-flow'); diff --git a/app/game-frontend/e2e/legacyArchiveViews.spec.ts b/app/game-frontend/e2e/legacyArchiveViews.spec.ts index 3fbac055..e2091f66 100644 --- a/app/game-frontend/e2e/legacyArchiveViews.spec.ts +++ b/app/game-frontend/e2e/legacyArchiveViews.spec.ts @@ -18,7 +18,7 @@ const installArchiveViews = async (page: Page) => { const legacy = isLegacyRequest(route); const results = operationNames(route).map((operation) => { if (operation === 'auth.status') return response({ ok: true }); - if (operation === 'lobby.info') return response({ myGeneral: null }); + if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '기록장수' } }); if (operation === 'ranking.getHallOfFameOptions') { return response([ { @@ -128,6 +128,58 @@ const installArchiveViews = async (page: Page) => { nations: [], }); } + if (operation === 'yearbook.getRange') { + return response({ firstYearMonth: 22001, lastYearMonth: 22001, currentYearMonth: 22001 }); + } + if (operation === 'public.getMapLayout') { + return response({ mapName: 'che', cityList: [], regionMap: {}, levelMap: {} }); + } + if (operation === 'yearbook.getHistory') { + return response({ + notModified: false, + hash: 'nation-color-contrast', + data: { + year: 220, + month: 1, + map: { + result: true, + version: 0, + startYear: 180, + year: 220, + month: 1, + techLevelLimit: { maxLevel: 12, initialLevel: 1, increaseYears: 5 }, + cityList: [], + nationList: [], + spyList: {}, + shownByGeneralList: [], + myCity: null, + myNation: null, + }, + nations: [ + { + id: 1, + name: '암국', + color: '#008000', + level: 1, + power: 100, + generalCount: 1, + cities: ['업'], + }, + { + id: 2, + name: '명국', + color: '#FFFF00', + level: 1, + power: 90, + generalCount: 1, + cities: ['허창'], + }, + ], + globalHistory: [], + globalAction: [], + }, + }); + } return { error: { message: `unhandled ${operation}`, data: { code: 'BAD_REQUEST' } } }; }); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) }); @@ -160,3 +212,20 @@ test('왕조 일람과 상세는 이전 서버 source와 profile을 유지한다 await expect(page.getByText(/이전 1기.*HWE 이전 서버/)).toBeVisible(); await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px'); }); + +test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => { + await installArchiveViews(page); + await page.setViewportSize({ width: 1200, height: 800 }); + await page.goto('yearbook'); + + const labels = page.locator('.nation-position tbody td:first-child span'); + await expect(labels).toHaveCount(2); + await expect(labels.filter({ hasText: '암국' })).toHaveCSS('color', 'rgb(255, 255, 255)'); + await expect(labels.filter({ hasText: '명국' })).toHaveCSS('color', 'rgb(0, 0, 0)'); + await page.screenshot({ path: testInfo.outputPath('yearbook-nation-contrast-desktop.png'), fullPage: true }); + + await page.setViewportSize({ width: 500, height: 800 }); + await expect(labels.filter({ hasText: '암국' })).toHaveCSS('color', 'rgb(255, 255, 255)'); + await expect(labels.filter({ hasText: '명국' })).toHaveCSS('color', 'rgb(0, 0, 0)'); + await page.screenshot({ path: testInfo.outputPath('yearbook-nation-contrast-mobile.png'), fullPage: true }); +}); diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index b3dc3348..4e53a936 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -1455,18 +1455,29 @@ test('main general card uses local turn time and command clock tracks corrected await expect(frozenClock).toHaveText('09:08:30'); }); -test('pure NPC message senders are not rendered as reply targets', async ({ page }) => { - const target = (generalId: number, generalName: string) => ({ +test('message targets keep reply behavior and use nation-color contrast in labels and select options', async ({ + page, +}) => { + const target = (generalId: number, generalName: string, color = '#008000', nationId = 1, nationName = '위') => ({ generalId, generalName, - nationId: 1, - nationName: '위', - color: '#008000', + nationId, + nationName, + color, icon: '', }); const messages = { ...emptyMessages(0), public: [ + { + id: 103, + text: '밝은 국가 메시지', + time: '2026-08-12 12:01:00', + msgType: 'public', + src: target(23, '밝은장수', '#FFFF00', 2, '밝은국'), + dest: null, + option: {}, + }, { id: 102, text: 'NPC 메시지', @@ -1505,6 +1516,13 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page color: '#008000', general: [[21, '유저장수', 0]], }, + { + nationId: 2, + mailbox: 9002, + name: '밝은국', + color: '#FFFF00', + general: [[23, '밝은장수', 0]], + }, ], }, }; @@ -1514,13 +1532,39 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page const npcMessage = page.locator('.desktop-message-panel .msg-plate[data-id="102"]'); const userMessage = page.locator('.desktop-message-panel .msg-plate[data-id="101"]'); + const brightMessage = page.locator('.desktop-message-panel .msg-plate[data-id="103"]'); await expect(npcMessage.locator('.msg-header')).toContainText('순수NPC:위'); await expect(npcMessage.locator('.msg-header')).not.toContainText('↩'); await expect(npcMessage.getByRole('button', { name: /순수NPC/ })).toHaveCount(0); await expect(userMessage.getByRole('button', { name: /유저장수:위.*↩/ })).toBeVisible(); + await expect(userMessage.locator('.msg-target')).toHaveCSS('color', 'rgb(255, 255, 255)'); + const brightTarget = brightMessage.locator('.msg-target'); + await expect(brightTarget).toHaveCSS('color', 'rgb(0, 0, 0)'); + await brightTarget.hover(); + await expect(brightTarget).toHaveCSS('color', 'rgb(0, 0, 0)'); + await brightTarget.focus(); + await expect(brightTarget).toHaveCSS('color', 'rgb(0, 0, 0)'); + await brightTarget.hover(); + await page.mouse.down(); + await expect(brightTarget).toHaveCSS('color', 'rgb(0, 0, 0)'); + await page.mouse.up(); + const mailbox = page.locator('.desktop-message-panel #mailbox_list'); + await expect(mailbox.locator('optgroup[label="위"]')).toHaveCSS('color', 'rgb(255, 255, 255)'); + await expect(mailbox.locator('optgroup[label="밝은국"]')).toHaveCSS('color', 'rgb(0, 0, 0)'); + await expect(mailbox.locator('option[value="23"]')).toHaveCSS('color', 'rgb(0, 0, 0)'); await userMessage.getByRole('button', { name: /유저장수:위.*↩/ }).click(); - await expect(page.locator('.desktop-message-panel #mailbox_list')).toHaveValue('21'); + await expect(mailbox).toHaveValue('21'); await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`); + + await page.setViewportSize({ width: 500, height: 900 }); + const mobilePanel = page.locator('.mobile-message-panel'); + await expect(mobilePanel.locator('.msg-plate[data-id="101"] .msg-target')).toHaveCSS( + 'color', + 'rgb(255, 255, 255)' + ); + await expect(mobilePanel.locator('.msg-plate[data-id="103"] .msg-target')).toHaveCSS('color', 'rgb(0, 0, 0)'); + await expect(mobilePanel.locator('#mailbox_list optgroup[label="밝은국"]')).toHaveCSS('color', 'rgb(0, 0, 0)'); + await persistArtifact(page, `${basePath.slice(1)}-message-nation-contrast-mobile-500`); }); test('main reserved-turn picker renders the Ref category order and raised button depth', async ({ page }) => { diff --git a/app/game-frontend/e2e/pastPlays.spec.ts b/app/game-frontend/e2e/pastPlays.spec.ts index aeae5744..5ee95071 100644 --- a/app/game-frontend/e2e/pastPlays.spec.ts +++ b/app/game-frontend/e2e/pastPlays.spec.ts @@ -9,7 +9,10 @@ const gameProfileId = gameProfile.split(':', 1)[0] ?? 'che'; const operationNames = (route: Route) => decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(','); -const installArchive = async (page: Page, options: { battleAvailable?: boolean; abandoned?: boolean } = {}) => { +const installArchive = async ( + page: Page, + options: { battleAvailable?: boolean; abandoned?: boolean; nationColor?: string } = {} +) => { const archiveRequestBodies: string[] = []; await page.addInitScript((profile) => { localStorage.setItem('sammo-game-token', 'ga_archive'); @@ -44,7 +47,7 @@ const installArchive = async (page: Page, options: { battleAvailable?: boolean; lastYearMonth: 21403, nationId: 2, nationName: '촉', - nationColor: '#800000', + nationColor: options.nationColor ?? '#800000', leadership: 91, strength: 98, intel: 77, @@ -69,7 +72,7 @@ const installArchive = async (page: Page, options: { battleAvailable?: boolean; serverId: 'che_2024_01', generalNo: 17, dynastyPath: '/dynasty/7?source=legacy', - nation: { name: '촉', color: '#800000' }, + nation: { name: '촉', color: options.nationColor ?? '#800000' }, general: { id: 17, name: '관우', @@ -175,6 +178,29 @@ test('지난 플레이 관직은 숫자 대신 저장된 Ref 표시명으로 나 await expect(generalRow).not.toContainText('che_'); }); +test('지난 플레이 국가 라벨은 밝은 국가색과 어두운 국가색에 대비되는 글자색을 쓴다', async ({ + page, +}, testInfo) => { + await installArchive(page, { nationColor: '#FFFF00' }); + await page.setViewportSize({ width: 1200, height: 900 }); + await page.goto('past-plays'); + + const nationLabel = page.locator('.season-card .nation-name').first(); + await expect(nationLabel).toHaveCSS('background-color', 'rgb(255, 255, 0)'); + await expect(nationLabel).toHaveCSS('color', 'rgb(0, 0, 0)'); + + await page.locator('.detail-toggle').click(); + const generalTitle = page.locator('[data-general-basic-card] .general-title'); + await expect(generalTitle).toHaveCSS('background-color', 'rgb(255, 255, 0)'); + await expect(generalTitle).toHaveCSS('color', 'rgb(0, 0, 0)'); + await page.screenshot({ path: testInfo.outputPath('past-play-nation-contrast-desktop.png'), fullPage: true }); + + await page.setViewportSize({ width: 500, height: 900 }); + await expect(nationLabel).toHaveCSS('color', 'rgb(0, 0, 0)'); + await expect(generalTitle).toHaveCSS('color', 'rgb(0, 0, 0)'); + await page.screenshot({ path: testInfo.outputPath('past-play-nation-contrast-mobile.png'), fullPage: true }); +}); + test('보존되지 않은 과거 전투 집계는 0으로 꾸미지 않고 가용성 경계를 표시한다', async ({ page }) => { await installArchive(page, { battleAvailable: false }); await page.goto('past-plays'); diff --git a/app/game-frontend/src/components/main/GeneralBasicCard.vue b/app/game-frontend/src/components/main/GeneralBasicCard.vue index 9956af33..3903bdf8 100644 --- a/app/game-frontend/src/components/main/GeneralBasicCard.vue +++ b/app/game-frontend/src/components/main/GeneralBasicCard.vue @@ -7,6 +7,7 @@ import { formatLocalTimeSeconds } from '../../utils/legacyDateTime'; import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress'; import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon'; import { configuredGameAssetUrl } from '../../utils/imageAssets'; +import { legacyLuminanceTextColor } from '../../utils/legacyNationColor'; interface GeneralStats { leadership: number; @@ -155,19 +156,11 @@ const injuryInfo = computed(() => { return { text: '건강', color: '#ffffff' }; }); -const isBrightColor = (color: string): boolean => { - const normalized = /^#[0-9a-f]{6}$/iu.test(color) ? color.slice(1) : '173d27'; - const red = Number.parseInt(normalized.slice(0, 2), 16); - const green = Number.parseInt(normalized.slice(2, 4), 16); - const blue = Number.parseInt(normalized.slice(4, 6), 16); - return (red * 299 + green * 587 + blue * 114) / 1000 >= 150; -}; - const titleStyle = computed(() => { const backgroundColor = props.nationColor || '#173d27'; return { backgroundColor, - color: isBrightColor(backgroundColor) ? '#000000' : '#ffffff', + color: legacyLuminanceTextColor(backgroundColor), }; }); diff --git a/app/game-frontend/src/components/main/MessagePanel.vue b/app/game-frontend/src/components/main/MessagePanel.vue index c51f378f..6275f273 100644 --- a/app/game-frontend/src/components/main/MessagePanel.vue +++ b/app/game-frontend/src/components/main/MessagePanel.vue @@ -1,6 +1,7 @@