merge: 최신 main을 세력일람 장수 hover 작업에 통합
This commit is contained in:
@@ -303,7 +303,7 @@ const install = async (
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: '적국',
|
name: '적국',
|
||||||
color: '#800000',
|
color: '#FFFF00',
|
||||||
capitalCityId: 2,
|
capitalCityId: 2,
|
||||||
level: 1,
|
level: 1,
|
||||||
power: 1000,
|
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('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()).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').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 geometry = await summary.evaluate((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
|
|||||||
],
|
],
|
||||||
warSpecials: [{ key: 'che_무쌍', name: '무쌍', info: '전투 특기' }],
|
warSpecials: [{ key: 'che_무쌍', name: '무쌍', info: '전투 특기' }],
|
||||||
nations: [
|
nations: [
|
||||||
{ id: 1, name: '촉', color: '#66aa44', scoutMessage: '함께 천하를 도모합시다.' },
|
{ id: 1, name: '촉', color: '#FFFF00', scoutMessage: '함께 천하를 도모합시다.' },
|
||||||
{ id: 2, name: '위', color: '#5577bb', scoutMessage: '능력 있는 장수를 기다립니다.' },
|
{ id: 2, name: '위', color: '#5577bb', scoutMessage: '능력 있는 장수를 기다립니다.' },
|
||||||
],
|
],
|
||||||
serverInfo: {
|
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.setViewportSize({ width: 1200, height: 900 });
|
||||||
await page.goto('join');
|
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 flow = page.locator('.join-flow');
|
||||||
const basicPanel = flow.locator('.panel-card').first();
|
const basicPanel = flow.locator('.panel-card').first();
|
||||||
const advanced = flow.locator('.advanced-options');
|
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.setViewportSize({ width: 390, height: 844 });
|
||||||
await page.goto('join');
|
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();
|
await expect(page.getByRole('heading', { name: '장수 기본 정보' })).toBeVisible();
|
||||||
const mobileGeometry = await page.evaluate(() => {
|
const mobileGeometry = await page.evaluate(() => {
|
||||||
const flow = document.querySelector<HTMLElement>('.join-flow');
|
const flow = document.querySelector<HTMLElement>('.join-flow');
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const installArchiveViews = async (page: Page) => {
|
|||||||
const legacy = isLegacyRequest(route);
|
const legacy = isLegacyRequest(route);
|
||||||
const results = operationNames(route).map((operation) => {
|
const results = operationNames(route).map((operation) => {
|
||||||
if (operation === 'auth.status') return response({ ok: true });
|
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') {
|
if (operation === 'ranking.getHallOfFameOptions') {
|
||||||
return response([
|
return response([
|
||||||
{
|
{
|
||||||
@@ -128,6 +128,58 @@ const installArchiveViews = async (page: Page) => {
|
|||||||
nations: [],
|
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' } } };
|
return { error: { message: `unhandled ${operation}`, data: { code: 'BAD_REQUEST' } } };
|
||||||
});
|
});
|
||||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
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.getByText(/이전 1기.*HWE 이전 서버/)).toBeVisible();
|
||||||
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
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 });
|
||||||
|
});
|
||||||
|
|||||||
@@ -1455,18 +1455,29 @@ test('main general card uses local turn time and command clock tracks corrected
|
|||||||
await expect(frozenClock).toHaveText('09:08:30');
|
await expect(frozenClock).toHaveText('09:08:30');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pure NPC message senders are not rendered as reply targets', async ({ page }) => {
|
test('message targets keep reply behavior and use nation-color contrast in labels and select options', async ({
|
||||||
const target = (generalId: number, generalName: string) => ({
|
page,
|
||||||
|
}) => {
|
||||||
|
const target = (generalId: number, generalName: string, color = '#008000', nationId = 1, nationName = '위') => ({
|
||||||
generalId,
|
generalId,
|
||||||
generalName,
|
generalName,
|
||||||
nationId: 1,
|
nationId,
|
||||||
nationName: '위',
|
nationName,
|
||||||
color: '#008000',
|
color,
|
||||||
icon: '',
|
icon: '',
|
||||||
});
|
});
|
||||||
const messages = {
|
const messages = {
|
||||||
...emptyMessages(0),
|
...emptyMessages(0),
|
||||||
public: [
|
public: [
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
text: '밝은 국가 메시지',
|
||||||
|
time: '2026-08-12 12:01:00',
|
||||||
|
msgType: 'public',
|
||||||
|
src: target(23, '밝은장수', '#FFFF00', 2, '밝은국'),
|
||||||
|
dest: null,
|
||||||
|
option: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 102,
|
id: 102,
|
||||||
text: 'NPC 메시지',
|
text: 'NPC 메시지',
|
||||||
@@ -1505,6 +1516,13 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page
|
|||||||
color: '#008000',
|
color: '#008000',
|
||||||
general: [[21, '유저장수', 0]],
|
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 npcMessage = page.locator('.desktop-message-panel .msg-plate[data-id="102"]');
|
||||||
const userMessage = page.locator('.desktop-message-panel .msg-plate[data-id="101"]');
|
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')).toContainText('순수NPC:위');
|
||||||
await expect(npcMessage.locator('.msg-header')).not.toContainText('↩');
|
await expect(npcMessage.locator('.msg-header')).not.toContainText('↩');
|
||||||
await expect(npcMessage.getByRole('button', { name: /순수NPC/ })).toHaveCount(0);
|
await expect(npcMessage.getByRole('button', { name: /순수NPC/ })).toHaveCount(0);
|
||||||
await expect(userMessage.getByRole('button', { name: /유저장수:위.*↩/ })).toBeVisible();
|
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 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 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 }) => {
|
test('main reserved-turn picker renders the Ref category order and raised button depth', async ({ page }) => {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ const gameProfileId = gameProfile.split(':', 1)[0] ?? 'che';
|
|||||||
const operationNames = (route: Route) =>
|
const operationNames = (route: Route) =>
|
||||||
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
|
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[] = [];
|
const archiveRequestBodies: string[] = [];
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
localStorage.setItem('sammo-game-token', 'ga_archive');
|
localStorage.setItem('sammo-game-token', 'ga_archive');
|
||||||
@@ -44,7 +47,7 @@ const installArchive = async (page: Page, options: { battleAvailable?: boolean;
|
|||||||
lastYearMonth: 21403,
|
lastYearMonth: 21403,
|
||||||
nationId: 2,
|
nationId: 2,
|
||||||
nationName: '촉',
|
nationName: '촉',
|
||||||
nationColor: '#800000',
|
nationColor: options.nationColor ?? '#800000',
|
||||||
leadership: 91,
|
leadership: 91,
|
||||||
strength: 98,
|
strength: 98,
|
||||||
intel: 77,
|
intel: 77,
|
||||||
@@ -69,7 +72,7 @@ const installArchive = async (page: Page, options: { battleAvailable?: boolean;
|
|||||||
serverId: 'che_2024_01',
|
serverId: 'che_2024_01',
|
||||||
generalNo: 17,
|
generalNo: 17,
|
||||||
dynastyPath: '/dynasty/7?source=legacy',
|
dynastyPath: '/dynasty/7?source=legacy',
|
||||||
nation: { name: '촉', color: '#800000' },
|
nation: { name: '촉', color: options.nationColor ?? '#800000' },
|
||||||
general: {
|
general: {
|
||||||
id: 17,
|
id: 17,
|
||||||
name: '관우',
|
name: '관우',
|
||||||
@@ -175,6 +178,29 @@ test('지난 플레이 관직은 숫자 대신 저장된 Ref 표시명으로 나
|
|||||||
await expect(generalRow).not.toContainText('che_');
|
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 }) => {
|
test('보존되지 않은 과거 전투 집계는 0으로 꾸미지 않고 가용성 경계를 표시한다', async ({ page }) => {
|
||||||
await installArchive(page, { battleAvailable: false });
|
await installArchive(page, { battleAvailable: false });
|
||||||
await page.goto('past-plays');
|
await page.goto('past-plays');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatLocalTimeSeconds } from '../../utils/legacyDateTime';
|
|||||||
import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress';
|
import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress';
|
||||||
import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon';
|
import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon';
|
||||||
import { configuredGameAssetUrl } from '../../utils/imageAssets';
|
import { configuredGameAssetUrl } from '../../utils/imageAssets';
|
||||||
|
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
|
||||||
|
|
||||||
interface GeneralStats {
|
interface GeneralStats {
|
||||||
leadership: number;
|
leadership: number;
|
||||||
@@ -155,19 +156,11 @@ const injuryInfo = computed(() => {
|
|||||||
return { text: '건강', color: '#ffffff' };
|
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 titleStyle = computed(() => {
|
||||||
const backgroundColor = props.nationColor || '#173d27';
|
const backgroundColor = props.nationColor || '#173d27';
|
||||||
return {
|
return {
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
color: isBrightColor(backgroundColor) ? '#000000' : '#ffffff',
|
color: legacyLuminanceTextColor(backgroundColor),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
import type { MessageType } from '@sammo-ts/logic';
|
import type { MessageType } from '@sammo-ts/logic';
|
||||||
|
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
|
||||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||||
import MessagePlate from './MessagePlate.vue';
|
import MessagePlate from './MessagePlate.vue';
|
||||||
|
|
||||||
@@ -172,14 +173,20 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
|||||||
v-for="group in mailboxGroups"
|
v-for="group in mailboxGroups"
|
||||||
:key="group.label"
|
:key="group.label"
|
||||||
:label="group.label"
|
:label="group.label"
|
||||||
:style="{ backgroundColor: group.color ?? '#000000', color: '#ffffff' }"
|
:style="{
|
||||||
|
backgroundColor: group.color ?? '#000000',
|
||||||
|
color: legacyLuminanceTextColor(group.color ?? '#000000'),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in group.options"
|
v-for="option in group.options"
|
||||||
:key="`${group.label}-${option.value}`"
|
:key="`${group.label}-${option.value}`"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
:disabled="option.disabled"
|
:disabled="option.disabled"
|
||||||
:style="{ backgroundColor: option.color ?? '#000000', color: '#ffffff' }"
|
:style="{
|
||||||
|
backgroundColor: option.color ?? '#000000',
|
||||||
|
color: legacyLuminanceTextColor(option.color ?? '#000000'),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import type { MessageType } from '@sammo-ts/logic';
|
import type { MessageType } from '@sammo-ts/logic';
|
||||||
import { DEFAULT_GENERAL_ICON_URL, resolveMessageGeneralIconUrl, useDefaultGeneralIcon } from '../../utils/generalIcon';
|
import { DEFAULT_GENERAL_ICON_URL, resolveMessageGeneralIconUrl, useDefaultGeneralIcon } from '../../utils/generalIcon';
|
||||||
|
import { isLegacyNationColorBright } from '../../utils/legacyNationColor';
|
||||||
|
|
||||||
interface MessageTarget {
|
interface MessageTarget {
|
||||||
generalId: number;
|
generalId: number;
|
||||||
@@ -95,25 +96,13 @@ const scheduleDeleteExpiry = () => {
|
|||||||
}, delay);
|
}, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isBright = (color: string): boolean => {
|
|
||||||
const match = /^#([0-9a-f]{6})$/i.exec(color);
|
|
||||||
if (!match) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const value = Number.parseInt(match[1]!, 16);
|
|
||||||
const red = (value >> 16) & 0xff;
|
|
||||||
const green = (value >> 8) & 0xff;
|
|
||||||
const blue = value & 0xff;
|
|
||||||
return red * 0.299 + green * 0.587 + blue * 0.114 > 160;
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconUrl = computed(() => resolveMessageGeneralIconUrl(props.message.src.icon));
|
const iconUrl = computed(() => resolveMessageGeneralIconUrl(props.message.src.icon));
|
||||||
const canReplyToGeneral = (target: MessageTarget): boolean => props.replyableGeneralIds.includes(target.generalId);
|
const canReplyToGeneral = (target: MessageTarget): boolean => props.replyableGeneralIds.includes(target.generalId);
|
||||||
|
|
||||||
const targetClass = (target: MessageTarget) => ({
|
const targetClass = (target: MessageTarget) => ({
|
||||||
'msg-target': true,
|
'msg-target': true,
|
||||||
'msg-bright': isBright(target.color),
|
'msg-bright': isLegacyNationColorBright(target.color),
|
||||||
'msg-dark': !isBright(target.color),
|
'msg-dark': !isLegacyNationColorBright(target.color),
|
||||||
});
|
});
|
||||||
|
|
||||||
const setTarget = (target: MessageTarget) => {
|
const setTarget = (target: MessageTarget) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||||
import { legacyNationTextColor } from '../../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
|
||||||
import { formatOfficerLevelText } from '../../utils/nationFormat';
|
import { formatOfficerLevelText } from '../../utils/nationFormat';
|
||||||
import { getNpcColor } from '../../utils/npcColor';
|
import { getNpcColor } from '../../utils/npcColor';
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ const displayChiefName = (chief: NationChief | undefined): string => {
|
|||||||
<div v-else class="nation-grid">
|
<div v-else class="nation-grid">
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="title"
|
||||||
:style="{ backgroundColor: props.nation.color, color: legacyNationTextColor(props.nation.color) }"
|
:style="{ backgroundColor: props.nation.color, color: legacyLuminanceTextColor(props.nation.color) }"
|
||||||
>
|
>
|
||||||
{{ props.nation.name }}
|
{{ props.nation.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,3 +21,17 @@ const lightTextBackgrounds = new Set([
|
|||||||
|
|
||||||
export const legacyNationTextColor = (backgroundColor: string): '#FFFFFF' | '#000000' =>
|
export const legacyNationTextColor = (backgroundColor: string): '#FFFFFF' | '#000000' =>
|
||||||
lightTextBackgrounds.has(backgroundColor.toUpperCase()) ? '#FFFFFF' : '#000000';
|
lightTextBackgrounds.has(backgroundColor.toUpperCase()) ? '#FFFFFF' : '#000000';
|
||||||
|
|
||||||
|
/** Mirrors Ref `isBrightColor()`, used by Vue nation labels and message targets. */
|
||||||
|
export const isLegacyNationColorBright = (backgroundColor: string): boolean => {
|
||||||
|
const normalized = backgroundColor.trim().replace(/^#/u, '');
|
||||||
|
if (!/^[0-9a-f]{6}$/iu.test(normalized)) return false;
|
||||||
|
|
||||||
|
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 * 0.299 + green * 0.587 + blue * 0.114 > 140;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const legacyLuminanceTextColor = (backgroundColor: string): '#000000' | '#FFFFFF' =>
|
||||||
|
isLegacyNationColorBright(backgroundColor) ? '#000000' : '#FFFFFF';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import { cityLevelMap, formatOfficerLevelText, regionMap } from '../utils/nationFormat';
|
import { cityLevelMap, formatOfficerLevelText, regionMap } from '../utils/nationFormat';
|
||||||
import { getNpcColor } from '../utils/npcColor';
|
import { getNpcColor } from '../utils/npcColor';
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.world.getCurrentCity.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.world.getCurrentCity.query>>;
|
||||||
@@ -57,31 +58,11 @@ const populationRate = computed(() => {
|
|||||||
if (!city.value || city.value.population === null) return '?';
|
if (!city.value || city.value.population === null) return '?';
|
||||||
return String(Math.round((city.value.population / city.value.populationMax) * 10_000) / 100);
|
return String(Math.round((city.value.population / city.value.populationMax) * 10_000) / 100);
|
||||||
});
|
});
|
||||||
const contrastColors = new Set([
|
|
||||||
'',
|
|
||||||
'#330000',
|
|
||||||
'#FF0000',
|
|
||||||
'#800000',
|
|
||||||
'#A0522D',
|
|
||||||
'#FF6347',
|
|
||||||
'#808000',
|
|
||||||
'#008000',
|
|
||||||
'#2E8B57',
|
|
||||||
'#008080',
|
|
||||||
'#6495ED',
|
|
||||||
'#0000FF',
|
|
||||||
'#000080',
|
|
||||||
'#483D8B',
|
|
||||||
'#7B68EE',
|
|
||||||
'#800080',
|
|
||||||
'#A9A9A9',
|
|
||||||
'#000000',
|
|
||||||
]);
|
|
||||||
const cityTitleStyle = computed(() => {
|
const cityTitleStyle = computed(() => {
|
||||||
const backgroundColor = city.value?.nationColor.toUpperCase() ?? '#000000';
|
const backgroundColor = city.value?.nationColor.toUpperCase() ?? '#000000';
|
||||||
return {
|
return {
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
color: contrastColors.has(backgroundColor) ? '#FFFFFF' : '#000000',
|
color: legacyNationTextColor(backgroundColor),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const woundedStat = (value: number, injury: number) =>
|
const woundedStat = (value: number, injury: number) =>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useRouter } from 'vue-router';
|
|||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
||||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||||
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
|
|
||||||
type DiplomacyResponse = Awaited<ReturnType<typeof trpc.diplomacy.getLetters.query>>;
|
type DiplomacyResponse = Awaited<ReturnType<typeof trpc.diplomacy.getLetters.query>>;
|
||||||
type DiplomacyLetter = DiplomacyResponse['letters'][number];
|
type DiplomacyLetter = DiplomacyResponse['letters'][number];
|
||||||
@@ -212,18 +213,9 @@ const stateOptionLabelMap: Record<string, string> = {
|
|||||||
const targetNation = (letter: DiplomacyLetter) =>
|
const targetNation = (letter: DiplomacyLetter) =>
|
||||||
letter.src.nationId === data.value?.myNationId ? letter.dest : letter.src;
|
letter.src.nationId === data.value?.myNationId ? letter.dest : letter.src;
|
||||||
|
|
||||||
const isBrightColor = (color: string): boolean => {
|
|
||||||
const normalized = color.trim().replace(/^#/u, '');
|
|
||||||
if (!/^[0-9a-f]{6}$/iu.test(normalized)) return false;
|
|
||||||
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 * 0.299 + green * 0.587 + blue * 0.114 > 170;
|
|
||||||
};
|
|
||||||
|
|
||||||
const nationStyle = (color: string) => ({
|
const nationStyle = (color: string) => ({
|
||||||
backgroundColor: color || '#315f86',
|
backgroundColor: color || '#315f86',
|
||||||
color: isBrightColor(color) ? '#000' : '#fff',
|
color: legacyLuminanceTextColor(color || '#315f86'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const signerIcon = (signer: DiplomacyLetter['src'] | DiplomacyLetter['dest']): string | null => {
|
const signerIcon = (signer: DiplomacyLetter['src'] | DiplomacyLetter['dest']): string | null => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import MapViewer from '../components/main/MapViewer.vue';
|
import MapViewer from '../components/main/MapViewer.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.world.getGlobalInfo.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.world.getGlobalInfo.query>>;
|
||||||
type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
||||||
@@ -19,7 +19,7 @@ const stateClass = (value: number) => `state-${value}`;
|
|||||||
const nationMap = computed(() => new Map(data.value?.nations.map((nation) => [nation.id, nation]) ?? []));
|
const nationMap = computed(() => new Map(data.value?.nations.map((nation) => [nation.id, nation]) ?? []));
|
||||||
const nationNameStyle = (color: string) => ({
|
const nationNameStyle = (color: string) => ({
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
color: legacyNationTextColor(color),
|
color: legacyLuminanceTextColor(color),
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
matrixElement,
|
matrixElement,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getNpcColor } from '../utils/npcColor';
|
|||||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
import { abilityLeadint, abilityLeadpow, abilityPowint, abilityRand, type GeneralStats } from '../utils/generalStats';
|
import { abilityLeadint, abilityLeadpow, abilityPowint, abilityRand, type GeneralStats } from '../utils/generalStats';
|
||||||
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
|
|
||||||
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
||||||
type JoinInput = Parameters<typeof trpc.join.createGeneral.mutate>[0];
|
type JoinInput = Parameters<typeof trpc.join.createGeneral.mutate>[0];
|
||||||
@@ -945,7 +946,15 @@ onUnmounted(() => {
|
|||||||
<div v-if="nationList.length === 0" class="muted">국가 정보가 아직 준비되지 않았습니다.</div>
|
<div v-if="nationList.length === 0" class="muted">국가 정보가 아직 준비되지 않았습니다.</div>
|
||||||
<div v-else class="nation-list">
|
<div v-else class="nation-list">
|
||||||
<article v-for="nation in nationList" :key="nation.id" class="nation-card">
|
<article v-for="nation in nationList" :key="nation.id" class="nation-card">
|
||||||
<h3 class="nation-name" :style="{ backgroundColor: nation.color }">{{ nation.name }}</h3>
|
<h3
|
||||||
|
class="nation-name"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: nation.color,
|
||||||
|
color: legacyLuminanceTextColor(nation.color),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ nation.name }}
|
||||||
|
</h3>
|
||||||
<p class="nation-message">{{ nation.scoutMessage ?? '권유문 없음' }}</p>
|
<p class="nation-message">{{ nation.scoutMessage ?? '권유문 없음' }}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable
|
|||||||
import type { GeneralDirectoryGeneral } from '../types/directory';
|
import type { GeneralDirectoryGeneral } from '../types/directory';
|
||||||
import { formatNationLevelText, formatOfficerLevelText } from '../utils/nationFormat';
|
import { formatNationLevelText, formatOfficerLevelText } from '../utils/nationFormat';
|
||||||
import { getNpcColor } from '../utils/npcColor';
|
import { getNpcColor } from '../utils/npcColor';
|
||||||
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
type Directory = Awaited<ReturnType<typeof trpc.world.getNationDirectory.query>>;
|
type Directory = Awaited<ReturnType<typeof trpc.world.getNationDirectory.query>>;
|
||||||
@@ -27,27 +28,6 @@ const activeGeneral = computed(() =>
|
|||||||
activeGeneralId.value === null ? null : (generalDetailsById.value.get(activeGeneralId.value) ?? null)
|
activeGeneralId.value === null ? null : (generalDetailsById.value.get(activeGeneralId.value) ?? null)
|
||||||
);
|
);
|
||||||
|
|
||||||
const whiteTextColors = new Set([
|
|
||||||
'',
|
|
||||||
'#330000',
|
|
||||||
'#ff0000',
|
|
||||||
'#800000',
|
|
||||||
'#a0522d',
|
|
||||||
'#ff6347',
|
|
||||||
'#808000',
|
|
||||||
'#008000',
|
|
||||||
'#2e8b57',
|
|
||||||
'#008080',
|
|
||||||
'#6495ed',
|
|
||||||
'#0000ff',
|
|
||||||
'#000080',
|
|
||||||
'#483d8b',
|
|
||||||
'#7b68ee',
|
|
||||||
'#800080',
|
|
||||||
'#a9a9a9',
|
|
||||||
'#000000',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const loadDirectory = async () => {
|
const loadDirectory = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
@@ -60,8 +40,6 @@ const loadDirectory = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerTextColor = (color: string): string => (whiteTextColors.has(color.toLowerCase()) ? '#ffffff' : '#000000');
|
|
||||||
|
|
||||||
const officerName = (nation: Nation, officerLevel: number) =>
|
const officerName = (nation: Nation, officerLevel: number) =>
|
||||||
nation.officers.find((officer) => officer.officerLevel === officerLevel)?.general;
|
nation.officers.find((officer) => officer.officerLevel === officerLevel)?.general;
|
||||||
const displayGeneralName = (general: { name: string; npcState: number }) =>
|
const displayGeneralName = (general: { name: string; npcState: number }) =>
|
||||||
@@ -149,7 +127,7 @@ onMounted(() => {
|
|||||||
<td
|
<td
|
||||||
colspan="8"
|
colspan="8"
|
||||||
class="center nation-title"
|
class="center nation-title"
|
||||||
:style="{ color: headerTextColor(nation.color), backgroundColor: nation.color }"
|
:style="{ color: legacyNationTextColor(nation.color), backgroundColor: nation.color }"
|
||||||
>
|
>
|
||||||
【 {{ nation.name }} 】
|
【 {{ nation.name }} 】
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import LegacyGeneralProgress from '../components/ui/LegacyGeneralProgress.vue';
|
import LegacyGeneralProgress from '../components/ui/LegacyGeneralProgress.vue';
|
||||||
import PanelCard from '../components/ui/PanelCard.vue';
|
import PanelCard from '../components/ui/PanelCard.vue';
|
||||||
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||||
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
type Archive = Awaited<ReturnType<typeof trpc.archive.myPastPlays.query>>;
|
type Archive = Awaited<ReturnType<typeof trpc.archive.myPastPlays.query>>;
|
||||||
@@ -282,7 +283,10 @@ onMounted(() => {
|
|||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
class="nation-name"
|
class="nation-name"
|
||||||
:style="{ backgroundColor: general.nationColor, color: '#fff' }"
|
:style="{
|
||||||
|
backgroundColor: general.nationColor,
|
||||||
|
color: legacyLuminanceTextColor(general.nationColor),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ general.nationName }}
|
{{ general.nationName }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useGameFeedback } from '../composables/useGameFeedback';
|
|||||||
import { useSessionStore } from '../stores/session';
|
import { useSessionStore } from '../stores/session';
|
||||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
||||||
@@ -144,29 +145,6 @@ const personalityName = (key: string | null): string | null => {
|
|||||||
const personalityInfo = (key: string | null): string =>
|
const personalityInfo = (key: string | null): string =>
|
||||||
key ? (personalities.value.find((entry) => entry.key === key)?.info ?? '') : '';
|
key ? (personalities.value.find((entry) => entry.key === key)?.info ?? '') : '';
|
||||||
|
|
||||||
const lightTextNationColors = new Set([
|
|
||||||
'',
|
|
||||||
'#330000',
|
|
||||||
'#FF0000',
|
|
||||||
'#800000',
|
|
||||||
'#A0522D',
|
|
||||||
'#FF6347',
|
|
||||||
'#808000',
|
|
||||||
'#008000',
|
|
||||||
'#2E8B57',
|
|
||||||
'#008080',
|
|
||||||
'#6495ED',
|
|
||||||
'#0000FF',
|
|
||||||
'#000080',
|
|
||||||
'#483D8B',
|
|
||||||
'#7B68EE',
|
|
||||||
'#800080',
|
|
||||||
'#A9A9A9',
|
|
||||||
'#000000',
|
|
||||||
]);
|
|
||||||
const nationTextColor = (color: string): string =>
|
|
||||||
lightTextNationColors.has(color.toUpperCase()) ? '#FFFFFF' : '#000000';
|
|
||||||
|
|
||||||
const selectCandidate = async (candidate: Candidate): Promise<void> => {
|
const selectCandidate = async (candidate: Candidate): Promise<void> => {
|
||||||
if (!hasGeneral.value) {
|
if (!hasGeneral.value) {
|
||||||
selectedUniqueName.value = candidate.uniqueName;
|
selectedUniqueName.value = candidate.uniqueName;
|
||||||
@@ -310,7 +288,7 @@ onBeforeUnmount(() => {
|
|||||||
v-for="nation in nations"
|
v-for="nation in nations"
|
||||||
:key="nation.id"
|
:key="nation.id"
|
||||||
:style="{
|
:style="{
|
||||||
color: nationTextColor(nation.color),
|
color: legacyLuminanceTextColor(nation.color),
|
||||||
backgroundColor: nation.color,
|
backgroundColor: nation.color,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
|
|
||||||
import MapViewer from '../components/main/MapViewer.vue';
|
import MapViewer from '../components/main/MapViewer.vue';
|
||||||
import { formatLog } from '../utils/formatLog';
|
import { formatLog } from '../utils/formatLog';
|
||||||
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
type YearbookRange = Awaited<ReturnType<typeof trpc.yearbook.getRange.query>>;
|
type YearbookRange = Awaited<ReturnType<typeof trpc.yearbook.getRange.query>>;
|
||||||
@@ -201,7 +202,13 @@ onMounted(async () => {
|
|||||||
:key="nation.id"
|
:key="nation.id"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<span :style="{ backgroundColor: nation.color }">{{ nation.name }}</span>
|
<span
|
||||||
|
:style="{
|
||||||
|
backgroundColor: nation.color,
|
||||||
|
color: legacyLuminanceTextColor(nation.color),
|
||||||
|
}"
|
||||||
|
>{{ nation.name }}</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ nation.power.toLocaleString() }}</td>
|
<td>{{ nation.power.toLocaleString() }}</td>
|
||||||
<td>{{ nation.generalCount.toLocaleString() }}</td>
|
<td>{{ nation.generalCount.toLocaleString() }}</td>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
isLegacyNationColorBright,
|
||||||
|
legacyLuminanceTextColor,
|
||||||
|
legacyNationTextColor,
|
||||||
|
} from '../src/utils/legacyNationColor.ts';
|
||||||
|
|
||||||
|
void describe('legacy nation color contrast', () => {
|
||||||
|
void it('matches Ref isBrightColor luminance and its strict threshold', () => {
|
||||||
|
assert.equal(isLegacyNationColorBright('#FFFF00'), true);
|
||||||
|
assert.equal(isLegacyNationColorBright('#008000'), false);
|
||||||
|
assert.equal(isLegacyNationColorBright('#20B2AA'), false);
|
||||||
|
assert.equal(isLegacyNationColorBright('#6495ED'), true);
|
||||||
|
assert.equal(isLegacyNationColorBright('not-a-color'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
void it('uses black text on bright backgrounds and white text on dark backgrounds', () => {
|
||||||
|
assert.equal(legacyLuminanceTextColor('#FFFF00'), '#000000');
|
||||||
|
assert.equal(legacyLuminanceTextColor('#008000'), '#FFFFFF');
|
||||||
|
assert.equal(legacyLuminanceTextColor('#A9A9A9'), '#000000');
|
||||||
|
});
|
||||||
|
|
||||||
|
void it('keeps the separate Ref PHP newColor palette contract', () => {
|
||||||
|
assert.equal(legacyNationTextColor('#A9A9A9'), '#FFFFFF');
|
||||||
|
assert.equal(legacyNationTextColor('#FFFF00'), '#000000');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user