fix(frontend): NPC 장수명 색상을 Ref와 일치시킨다
암행부와 국가 장수 목록의 잘못되거나 누락된 색상을 공통 팔레트로 통일한다. 명령 대상, 토너먼트와 베팅 응답에도 npcState를 전달해 같은 표시 계약을 적용한다. 데스크톱과 모바일 Chromium 회귀 테스트로 상태별 색상을 검증한다.
This commit is contained in:
@@ -160,7 +160,7 @@ export const tournamentRouter = router({
|
||||
? []
|
||||
: await ctx.db.general.findMany({
|
||||
where: { id: { in: participantIds } },
|
||||
select: { id: true, picture: true, imageServer: true },
|
||||
select: { id: true, picture: true, imageServer: true, npcState: true },
|
||||
});
|
||||
const iconsByGeneralId = new Map(iconRows.map((general) => [general.id, general]));
|
||||
const publicParticipants = participants.map((participant) => {
|
||||
@@ -169,6 +169,7 @@ export const tournamentRouter = router({
|
||||
...participant,
|
||||
picture: icon?.picture ?? null,
|
||||
imageServer: icon?.imageServer ?? 0,
|
||||
npcState: icon?.npcState ?? 0,
|
||||
};
|
||||
});
|
||||
return { state, participants: publicParticipants, matches, betCount: bets.length, sourceRevision };
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface TurnCommandOption {
|
||||
rice?: number;
|
||||
crew?: number;
|
||||
troopId?: number;
|
||||
npcState?: number;
|
||||
}
|
||||
|
||||
export interface TurnCommandAmountPreset {
|
||||
|
||||
@@ -83,6 +83,7 @@ export const buildRefGeneralTargetOptions = (options: {
|
||||
...(entry.rice === undefined ? {} : { rice: entry.rice }),
|
||||
...(entry.crew === undefined ? {} : { crew: entry.crew }),
|
||||
...(entry.troopId === undefined ? {} : { troopId: entry.troopId }),
|
||||
npcState: entry.npcState,
|
||||
};
|
||||
};
|
||||
const project = (predicate: (entry: GeneralTargetSource) => boolean): TurnCommandOption[] =>
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('Ref command general targets', () => {
|
||||
expect(ids('che_등용')).toEqual([4]);
|
||||
expect(ids('che_장수대상임관')).toEqual([2, 3, 4, 5]);
|
||||
expect(result.generals.map((entry) => entry.value)).toEqual([1, 2, 4]);
|
||||
expect(result.generalTargets.che_포상?.map((entry) => entry.npcState)).toEqual([0, 0, 2]);
|
||||
});
|
||||
|
||||
it('adds resource, crew, and troop details and puts actual troop members first for kick orders', () => {
|
||||
|
||||
@@ -146,6 +146,7 @@ const inputOptions = {
|
||||
{
|
||||
value: 3,
|
||||
label: '여포NPC (아국 · 업)',
|
||||
npcState: 2,
|
||||
gold: 3000,
|
||||
rice: 500,
|
||||
crew: 1500,
|
||||
@@ -2221,6 +2222,10 @@ test('offers Ref amount presets and rich, command-specific general lists', async
|
||||
'여포NPC (아국 · 업)',
|
||||
'장수 (아국 · 업)',
|
||||
]);
|
||||
await expect(generalList.locator('.target-option').filter({ hasText: '여포NPC' }).locator('strong')).toHaveCSS(
|
||||
'color',
|
||||
'rgb(0, 255, 255)'
|
||||
);
|
||||
await expect(generalList).toContainText('금 100 · 쌀 4,000 · 병력 1,200 · 탑승 부대 청룡대 (부대장)');
|
||||
await form.getByRole('button', { name: '쌀', exact: true }).click();
|
||||
await expect(generalList.locator('.target-option strong')).toHaveText([
|
||||
|
||||
@@ -77,7 +77,38 @@ const otherGeneral = {
|
||||
belong: 4,
|
||||
refreshScoreTotal: 20,
|
||||
};
|
||||
const install = async (page: Page, secretAllowed = true) => {
|
||||
const npcColorStates = [0, 1, 2, 4, 5, 6] as const;
|
||||
const npcColorGenerals = npcColorStates.map((npcState) => ({
|
||||
...general,
|
||||
id: 100 + npcState,
|
||||
name: `색상장수${npcState}`,
|
||||
npcState,
|
||||
}));
|
||||
const npcColorSecretGenerals = npcColorStates.map((npcState) => ({
|
||||
id: 100 + npcState,
|
||||
name: `색상장수${npcState}`,
|
||||
npcState,
|
||||
injury: 0,
|
||||
stats: { leadership: 70, strength: 60, intelligence: 50 },
|
||||
leadershipBonus: 0,
|
||||
experienceLevel: 9,
|
||||
troopId: 0,
|
||||
troopName: null,
|
||||
gold: 1000,
|
||||
rice: 2000,
|
||||
cityId: 1,
|
||||
cityName: '업',
|
||||
defenceTrain: 90,
|
||||
defenceTrainText: '☆',
|
||||
crewTypeId: 1,
|
||||
crew: 300,
|
||||
train: 90,
|
||||
atmos: 90,
|
||||
killTurn: 7,
|
||||
turnTime: `2026-01-01T01:0${npcState}:00.000Z`,
|
||||
reservedCommands: [],
|
||||
}));
|
||||
const install = async (page: Page, secretAllowed = true, npcColorFixture = false) => {
|
||||
await page.addInitScript((profile) => {
|
||||
localStorage.setItem('sammo-game-token', 'ga_general');
|
||||
localStorage.setItem('sammo-game-profile', profile);
|
||||
@@ -91,7 +122,7 @@ const install = async (page: Page, secretAllowed = true) => {
|
||||
return response({
|
||||
nation: { id: 1, name: '위', color: '#008000', level: 3 },
|
||||
viewer: { generalId: 1, permission: 0 },
|
||||
generals: [general, otherGeneral],
|
||||
generals: npcColorFixture ? npcColorGenerals : [general, otherGeneral],
|
||||
});
|
||||
if (operation === 'nation.getSecretGeneralList') {
|
||||
if (!secretAllowed)
|
||||
@@ -118,62 +149,64 @@ const install = async (page: Page, secretAllowed = true) => {
|
||||
60: { crew: 300, generals: 1 },
|
||||
},
|
||||
},
|
||||
generals: [
|
||||
{
|
||||
id: 1,
|
||||
name: '테스트장수',
|
||||
npcState: 0,
|
||||
injury: 0,
|
||||
stats: { leadership: 70, strength: 60, intelligence: 50 },
|
||||
leadershipBonus: 0,
|
||||
experienceLevel: 9,
|
||||
troopId: 0,
|
||||
troopName: null,
|
||||
gold: 1000,
|
||||
rice: 2000,
|
||||
cityId: 1,
|
||||
cityName: '업',
|
||||
defenceTrain: 90,
|
||||
defenceTrainText: '☆',
|
||||
crewTypeId: 1,
|
||||
crew: 300,
|
||||
train: 90,
|
||||
atmos: 90,
|
||||
killTurn: 7,
|
||||
turnTime: '2026-01-01T01:02:00.000Z',
|
||||
reservedCommands: [
|
||||
{ action: 'che_이동', args: { destCityId: 1 } },
|
||||
{ action: 'che_징병', args: { crewType: 1, amount: 300 } },
|
||||
{ action: 'che_증여', args: { destGeneralId: 2, isGold: false, amount: 200 } },
|
||||
{ action: 'che_화계', args: { destCityId: 1 } },
|
||||
{ action: '휴식', args: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '부유장수',
|
||||
npcState: 0,
|
||||
injury: 0,
|
||||
stats: { leadership: 60, strength: 50, intelligence: 40 },
|
||||
leadershipBonus: 0,
|
||||
experienceLevel: 8,
|
||||
troopId: 1,
|
||||
troopName: '제1부대',
|
||||
gold: 3000,
|
||||
rice: 1000,
|
||||
cityId: 2,
|
||||
cityName: '낙양',
|
||||
defenceTrain: 80,
|
||||
defenceTrainText: '◎',
|
||||
crewTypeId: 2,
|
||||
crew: 100,
|
||||
train: 80,
|
||||
atmos: 80,
|
||||
killTurn: 3,
|
||||
turnTime: '2026-01-01T02:02:00.000Z',
|
||||
reservedCommands: [],
|
||||
},
|
||||
],
|
||||
generals: npcColorFixture
|
||||
? npcColorSecretGenerals
|
||||
: [
|
||||
{
|
||||
id: 1,
|
||||
name: '테스트장수',
|
||||
npcState: 0,
|
||||
injury: 0,
|
||||
stats: { leadership: 70, strength: 60, intelligence: 50 },
|
||||
leadershipBonus: 0,
|
||||
experienceLevel: 9,
|
||||
troopId: 0,
|
||||
troopName: null,
|
||||
gold: 1000,
|
||||
rice: 2000,
|
||||
cityId: 1,
|
||||
cityName: '업',
|
||||
defenceTrain: 90,
|
||||
defenceTrainText: '☆',
|
||||
crewTypeId: 1,
|
||||
crew: 300,
|
||||
train: 90,
|
||||
atmos: 90,
|
||||
killTurn: 7,
|
||||
turnTime: '2026-01-01T01:02:00.000Z',
|
||||
reservedCommands: [
|
||||
{ action: 'che_이동', args: { destCityId: 1 } },
|
||||
{ action: 'che_징병', args: { crewType: 1, amount: 300 } },
|
||||
{ action: 'che_증여', args: { destGeneralId: 2, isGold: false, amount: 200 } },
|
||||
{ action: 'che_화계', args: { destCityId: 1 } },
|
||||
{ action: '휴식', args: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '부유장수',
|
||||
npcState: 0,
|
||||
injury: 0,
|
||||
stats: { leadership: 60, strength: 50, intelligence: 40 },
|
||||
leadershipBonus: 0,
|
||||
experienceLevel: 8,
|
||||
troopId: 1,
|
||||
troopName: '제1부대',
|
||||
gold: 3000,
|
||||
rice: 1000,
|
||||
cityId: 2,
|
||||
cityName: '낙양',
|
||||
defenceTrain: 80,
|
||||
defenceTrainText: '◎',
|
||||
crewTypeId: 2,
|
||||
crew: 100,
|
||||
train: 80,
|
||||
atmos: 80,
|
||||
killTurn: 3,
|
||||
turnTime: '2026-01-01T02:02:00.000Z',
|
||||
reservedCommands: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (operation === 'turns.getCommandTable') return response(commandTable);
|
||||
@@ -183,6 +216,44 @@ const install = async (page: Page, secretAllowed = true) => {
|
||||
});
|
||||
};
|
||||
|
||||
test('NPC general names use the complete Ref palette in secret and nation lists on desktop and mobile', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await install(page, true, true);
|
||||
const expectedColors = new Map<number, string>([
|
||||
[1, 'rgb(135, 206, 235)'],
|
||||
[2, 'rgb(0, 255, 255)'],
|
||||
[4, 'rgb(0, 191, 255)'],
|
||||
[5, 'rgb(0, 139, 139)'],
|
||||
[6, 'rgb(102, 205, 170)'],
|
||||
]);
|
||||
|
||||
for (const viewport of [
|
||||
{ width: 1200, height: 900 },
|
||||
{ width: 500, height: 900 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
for (const path of ['nation/secret', 'nation/generals']) {
|
||||
await page.goto(path);
|
||||
const table = page.locator(path.endsWith('secret') ? '#secret-general-list' : '#nation-general-list');
|
||||
await expect(table).toBeVisible();
|
||||
for (const npcState of npcColorStates) {
|
||||
const name = table.locator(`[data-npc-state="${npcState}"] [data-general-name]`);
|
||||
await expect(name).toBeVisible();
|
||||
if (npcState === 0) {
|
||||
expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe('');
|
||||
} else {
|
||||
await expect(name).toHaveCSS('color', expectedColors.get(npcState)!);
|
||||
}
|
||||
}
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath(`npc-colors-${path.replace('/', '-')}-${viewport.width}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('nation generals keeps the 1000px legacy grid and redacted member columns', async ({ page }) => {
|
||||
await install(page);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
|
||||
@@ -37,6 +37,7 @@ const names = [
|
||||
longGeneralName,
|
||||
...Array.from({ length: 48 }, (_, index) => `예선장수${index + 17}`),
|
||||
];
|
||||
const tournamentNpcStates = [0, 1, 2, 4, 5, 6] as const;
|
||||
const participants = names.map((name, index) => ({
|
||||
id: index + 1,
|
||||
name,
|
||||
@@ -46,6 +47,7 @@ const participants = names.map((name, index) => ({
|
||||
level: 10,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
npcState: tournamentNpcStates[index % tournamentNpcStates.length],
|
||||
groupId: Math.floor(index / 8) < 4 ? 10 + (index % 8) : index % 8,
|
||||
groupNo: Math.floor(index / 8),
|
||||
win: Math.floor(index / 8) < 4 ? 3 - (index % 2) : 7 - Math.floor(index / 8),
|
||||
@@ -305,7 +307,7 @@ const installFixture = async (
|
||||
name: participant.name,
|
||||
picture: participant.picture,
|
||||
imageServer: participant.imageServer,
|
||||
npcState: 0,
|
||||
npcState: participant.npcState,
|
||||
stat: 240 - index,
|
||||
games: 10,
|
||||
win: 7,
|
||||
@@ -368,6 +370,48 @@ const openTournament = async (page: Page) => {
|
||||
await expect(page.getByLabel('토너먼트 대진표')).toBeVisible();
|
||||
};
|
||||
|
||||
test('tournament and betting identities preserve Ref NPC name colors on desktop and mobile', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await installFixture(page, { tournamentStage: 7 });
|
||||
const expectedColors = [
|
||||
'',
|
||||
'rgb(135, 206, 235)',
|
||||
'rgb(0, 255, 255)',
|
||||
'rgb(0, 191, 255)',
|
||||
'rgb(0, 139, 139)',
|
||||
'rgb(102, 205, 170)',
|
||||
];
|
||||
|
||||
for (const viewport of [
|
||||
{ width: 1365, height: 900 },
|
||||
{ width: 500, height: 900 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto('tournament');
|
||||
const scope = viewport.width > 800 ? '.desktop-bracket' : '.mobile-bracket';
|
||||
for (let index = 0; index < expectedColors.length; index += 1) {
|
||||
const name = page.locator(`${scope} [data-general-id="${index + 1}"] .general-identity-name`).first();
|
||||
await expect(name).toBeVisible();
|
||||
if (expectedColors[index]) await expect(name).toHaveCSS('color', expectedColors[index]!);
|
||||
else expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe('');
|
||||
}
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath(`tournament-npc-colors-${viewport.width}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
await page.setViewportSize({ width: 1365, height: 900 });
|
||||
await page.goto('betting');
|
||||
const rankingNames = page.locator('.ranking-general .general-identity-name');
|
||||
for (let index = 0; index < expectedColors.length; index += 1) {
|
||||
if (expectedColors[index]) await expect(rankingNames.nth(index)).toHaveCSS('color', expectedColors[index]!);
|
||||
else expect(await rankingNames.nth(index).evaluate((element) => (element as HTMLElement).style.color)).toBe('');
|
||||
}
|
||||
await page.screenshot({ path: testInfo.outputPath('betting-npc-colors-1365.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('desktop bracket connects every real general slot to the next round', async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 1365, height: 900 });
|
||||
await openTournament(page);
|
||||
|
||||
@@ -8,6 +8,7 @@ export type CommandOption = {
|
||||
rice?: number;
|
||||
crew?: number;
|
||||
troopId?: number;
|
||||
npcState?: number;
|
||||
};
|
||||
|
||||
export type CommandAmountPreset = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type CommandArgumentFieldContract,
|
||||
} from '../command/commandArgumentDraft';
|
||||
import { legacyNationTextColor } from '../../utils/legacyNationColor';
|
||||
import { getNpcColor } from '../../utils/npcColor';
|
||||
import type {
|
||||
CommandInputContext,
|
||||
CommandInputField,
|
||||
@@ -141,11 +142,16 @@ const selectedOptionFor = (field: CommandInputField): CommandOption | undefined
|
||||
optionsFor(field).find((entry) => entry.value === values[field.key]);
|
||||
|
||||
const colorOptionStyle = (field: CommandInputField, option?: CommandOption): CSSProperties | undefined => {
|
||||
if (field.optionSource !== 'colors' || !option?.color) return undefined;
|
||||
return {
|
||||
backgroundColor: option.color,
|
||||
color: legacyNationTextColor(option.color),
|
||||
};
|
||||
if (field.optionSource === 'colors' && option?.color) {
|
||||
return {
|
||||
backgroundColor: option.color,
|
||||
color: legacyNationTextColor(option.color),
|
||||
};
|
||||
}
|
||||
if (field.optionSource === 'generals' && option?.npcState !== undefined) {
|
||||
return { color: getNpcColor(option.npcState) };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const cityTargetField = computed(() =>
|
||||
@@ -561,7 +567,7 @@ watch(
|
||||
@click="setSelectValue(field, String(option.value))"
|
||||
>
|
||||
<span v-if="option.color" class="option-color" :style="{ backgroundColor: option.color }" />
|
||||
<strong>{{ option.label }}</strong>
|
||||
<strong :style="colorOptionStyle(field, option)">{{ option.label }}</strong>
|
||||
<span class="target-state">{{
|
||||
option.availableNow === false ? '현재 불가' : option.availableNow ? '우선 대상' : '대상'
|
||||
}}</span>
|
||||
|
||||
@@ -134,7 +134,12 @@ const mobilePairs = computed(() => {
|
||||
top: `${slotY(columnIndex, slotIndex)}px`,
|
||||
}"
|
||||
>
|
||||
<GeneralIdentity :name="slot.name" :picture="slot.picture" :image-server="slot.imageServer" />
|
||||
<GeneralIdentity
|
||||
:name="slot.name"
|
||||
:picture="slot.picture"
|
||||
:image-server="slot.imageServer"
|
||||
:npc-state="slot.npcState"
|
||||
/>
|
||||
<button
|
||||
v-if="columnIndex === 0 && bettingOpen && slot.id !== null"
|
||||
type="button"
|
||||
@@ -182,7 +187,12 @@ const mobilePairs = computed(() => {
|
||||
}"
|
||||
:data-general-id="slot.id ?? undefined"
|
||||
>
|
||||
<GeneralIdentity :name="slot.name" :picture="slot.picture" :image-server="slot.imageServer" />
|
||||
<GeneralIdentity
|
||||
:name="slot.name"
|
||||
:picture="slot.picture"
|
||||
:image-server="slot.imageServer"
|
||||
:npc-state="slot.npcState"
|
||||
/>
|
||||
<button
|
||||
v-if="activeMobileRound === 0 && bettingOpen && slot.id !== null"
|
||||
type="button"
|
||||
|
||||
@@ -10,6 +10,7 @@ type StandingParticipant = {
|
||||
leadership: number;
|
||||
strength: number;
|
||||
intel: number;
|
||||
npcState?: number;
|
||||
win?: number;
|
||||
draw?: number;
|
||||
lose?: number;
|
||||
@@ -55,6 +56,7 @@ const pointsOf = (participant: StandingParticipant): number => (participant.win
|
||||
:name="slot.participant.name"
|
||||
:picture="slot.participant.picture"
|
||||
:image-server="slot.participant.imageServer"
|
||||
:npc-state="slot.participant.npcState"
|
||||
>
|
||||
<template #details>
|
||||
<span class="standing-summary">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon, type GeneralIconSource } from '../../utils/generalIcon';
|
||||
import { getNpcColor } from '../../utils/npcColor';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -9,12 +10,14 @@ const props = withDefaults(
|
||||
imageServer?: GeneralIconSource['imageServer'];
|
||||
hideIcon?: boolean;
|
||||
placeholder?: boolean;
|
||||
npcState?: number | null;
|
||||
}>(),
|
||||
{
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
hideIcon: false,
|
||||
placeholder: false,
|
||||
npcState: null,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -42,10 +45,14 @@ const iconUrl = computed(() =>
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-if="$slots.details" class="general-identity-copy">
|
||||
<span class="general-identity-name" :title="name">{{ name }}</span>
|
||||
<span class="general-identity-name" :title="name" :style="{ color: getNpcColor(npcState ?? 0) }">{{
|
||||
name
|
||||
}}</span>
|
||||
<span class="general-identity-details"><slot name="details" /></span>
|
||||
</span>
|
||||
<span v-else class="general-identity-name" :title="name">{{ name }}</span>
|
||||
<span v-else class="general-identity-name" :title="name" :style="{ color: getNpcColor(npcState ?? 0) }">{{
|
||||
name
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface TournamentBracketParticipant {
|
||||
leadership?: number;
|
||||
strength?: number;
|
||||
intel?: number;
|
||||
npcState?: number;
|
||||
}
|
||||
|
||||
export interface TournamentBracketMatch {
|
||||
@@ -26,6 +27,7 @@ export interface TournamentBracketSlot {
|
||||
leadership?: number;
|
||||
strength?: number;
|
||||
intel?: number;
|
||||
npcState?: number;
|
||||
}
|
||||
|
||||
export interface TournamentCoreStat {
|
||||
@@ -91,6 +93,7 @@ export const buildTournamentBracket = (
|
||||
leadership: participant?.leadership,
|
||||
strength: participant?.strength,
|
||||
intel: participant?.intel,
|
||||
npcState: participant?.npcState,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -113,6 +116,7 @@ export const buildTournamentBracket = (
|
||||
leadership: participantOf(resolvedWinnerId)?.leadership,
|
||||
strength: participantOf(resolvedWinnerId)?.strength,
|
||||
intel: participantOf(resolvedWinnerId)?.intel,
|
||||
npcState: participantOf(resolvedWinnerId)?.npcState,
|
||||
},
|
||||
final,
|
||||
semi: buildRound(9, 4),
|
||||
|
||||
@@ -167,6 +167,7 @@ const placeBet = async () => {
|
||||
:name="selectedTarget.name"
|
||||
:picture="selectedTarget.picture"
|
||||
:image-server="selectedTarget.imageServer"
|
||||
:npc-state="selectedTarget.npcState"
|
||||
/>
|
||||
<label class="bet-amount-field">
|
||||
<span>베팅 금액</span>
|
||||
@@ -260,6 +261,7 @@ const placeBet = async () => {
|
||||
:name="entry.name"
|
||||
:picture="entry.picture"
|
||||
:image-server="entry.imageServer"
|
||||
:npc-state="entry.npcState"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ entry.stat }}</td>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { formatOfficerLevelText } from '../utils/nationFormat';
|
||||
import { getNpcColor } from '../utils/npcColor';
|
||||
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
||||
import {
|
||||
DISPLAY_SETTINGS_KEY,
|
||||
@@ -770,14 +771,18 @@ onBeforeUnmount(() => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(general, index) in generals" :key="general.id" :data-general-id="general.id">
|
||||
<tr
|
||||
v-for="(general, index) in generals"
|
||||
:key="general.id"
|
||||
:data-general-id="general.id"
|
||||
:data-npc-state="general.npcState"
|
||||
>
|
||||
<td
|
||||
v-for="column in activeColumns"
|
||||
:key="column.id"
|
||||
:class="{
|
||||
'icon-cell': column.id === 'icon',
|
||||
'name-cell': column.id === 'name',
|
||||
[`npc-${general.npcState}`]: column.id === 'name',
|
||||
'numeric-cell':
|
||||
column.searchable === 'number' ||
|
||||
['goldRice_1', 'killturnAndRefresh_1'].includes(column.id),
|
||||
@@ -799,6 +804,11 @@ onBeforeUnmount(() => {
|
||||
}}<span v-if="visibleCrew(general) !== null">명</span>
|
||||
</template>
|
||||
<template v-else-if="column.id === 'belong'">{{ general.belong }}년</template>
|
||||
<template v-else-if="column.id === 'name'">
|
||||
<span data-general-name :style="{ color: getNpcColor(general.npcState) }">{{
|
||||
general.name
|
||||
}}</span>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="column.id === 'refreshScoreTotal' || column.id === 'killturnAndRefresh_1'"
|
||||
>
|
||||
@@ -1182,16 +1192,6 @@ td {
|
||||
.state {
|
||||
margin: 40px;
|
||||
}
|
||||
.npc-0,
|
||||
.npc-1 {
|
||||
color: skyblue;
|
||||
}
|
||||
.npc-2,
|
||||
.npc-3,
|
||||
.npc-4,
|
||||
.npc-5 {
|
||||
color: #aaa;
|
||||
}
|
||||
.error {
|
||||
color: #ff7373;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
||||
import type { CommandTable } from '../components/command/types';
|
||||
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
||||
import { getNpcColor } from '../utils/npcColor';
|
||||
import { trpc } from '../utils/trpc';
|
||||
type Result = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
||||
type ReservedCommand = Result['generals'][number]['reservedCommands'][number];
|
||||
@@ -221,8 +222,18 @@ onMounted(load);
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="general in generals" :key="general.id" :data-general-id="general.id">
|
||||
<td>{{ displayName(general) }}<br />Lv {{ general.experienceLevel }}</td>
|
||||
<tr
|
||||
v-for="general in generals"
|
||||
:key="general.id"
|
||||
:data-general-id="general.id"
|
||||
:data-npc-state="general.npcState"
|
||||
>
|
||||
<td>
|
||||
<span data-general-name :style="{ color: getNpcColor(general.npcState) }">{{
|
||||
displayName(general)
|
||||
}}</span
|
||||
><br />Lv {{ general.experienceLevel }}
|
||||
</td>
|
||||
<td>
|
||||
{{ general.stats.leadership
|
||||
}}<span v-if="general.leadershipBonus" class="bonus">+{{ general.leadershipBonus }}</span
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
||||
import { getNpcColor } from '../utils/npcColor';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
type NpcList = Awaited<ReturnType<typeof trpc.public.getNpcList.query>>;
|
||||
@@ -188,7 +189,7 @@ onMounted(() => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="general in data.generals" :key="general.id" :data-general-id="general.id">
|
||||
<td :class="{ possessed: general.npcState === 1 }">{{ general.name }}</td>
|
||||
<td :style="{ color: getNpcColor(general.npcState) }">{{ general.name }}</td>
|
||||
<td>{{ general.ownerName }}</td>
|
||||
<td>Lv {{ general.level }}</td>
|
||||
<td>{{ general.nationName }}</td>
|
||||
@@ -322,10 +323,6 @@ onMounted(() => {
|
||||
width: 78px;
|
||||
}
|
||||
|
||||
.possessed {
|
||||
color: skyblue;
|
||||
}
|
||||
|
||||
.trait-tooltip {
|
||||
position: relative;
|
||||
cursor: help;
|
||||
|
||||
Reference in New Issue
Block a user