fix(frontend): NPC 장수명 색상을 Ref와 일치시킨다

암행부와 국가 장수 목록의 잘못되거나 누락된 색상을 공통 팔레트로 통일한다.
명령 대상, 토너먼트와 베팅 응답에도 npcState를 전달해 같은 표시 계약을 적용한다.
데스크톱과 모바일 Chromium 회귀 테스트로 상태별 색상을 검증한다.
This commit is contained in:
2026-08-25 01:18:27 +00:00
parent 7af1f63a8e
commit 2d711d0bab
17 changed files with 253 additions and 89 deletions
@@ -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 -5
View File
@@ -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;