fix(frontend): NPC 장수명 색상을 Ref와 일치시킨다
암행부와 국가 장수 목록의 잘못되거나 누락된 색상을 공통 팔레트로 통일한다. 명령 대상, 토너먼트와 베팅 응답에도 npcState를 전달해 같은 표시 계약을 적용한다. 데스크톱과 모바일 Chromium 회귀 테스트로 상태별 색상을 검증한다.
This commit is contained in:
@@ -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