등용·장수대상임관 대상의 타국 장수 정보를 Ref 공개 범위로 제한하고 국가색 묶음 복원
- 명령표의 등용·장수대상임관·공통 장수 fallback에서 타국·재야 장수의 금·쌀·병력·훈련·사기·부대·위치 도시를 제외 - Ref exportJSVars 범위(이름·국가·NPC·통/무/지)만 싣고 국가 묶음용 nationId 추가 - 재야 actor에게는 같은 국가 명령 후보를 보내지 않음 - 등용·장수대상임관 후보를 Ref SelectGeneral groupByNation처럼 국가색 머리와 optgroup으로 묶음 - 증여·포상·몰수·부대탈퇴지시·선양 후보에 Ref의 통/무/지 표시 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor.ts';
|
||||
import type { CommandOption } from './types';
|
||||
|
||||
/** Ref `SelectGeneral.vue`에 `groupByNation`을 넘기는 명령. */
|
||||
export const NATION_GROUPED_GENERAL_COMMANDS: ReadonlySet<string> = new Set(['che_등용', 'che_장수대상임관']);
|
||||
|
||||
export type GeneralOptionGroup = {
|
||||
nationId: number;
|
||||
name: string;
|
||||
color: string;
|
||||
textColor: '#000000' | '#FFFFFF';
|
||||
options: CommandOption[];
|
||||
};
|
||||
|
||||
/** Ref `getNationStaticInfo(0)`의 재야 표시값. */
|
||||
const WANDERER = { name: '재야', color: '#000000' } as const;
|
||||
|
||||
/**
|
||||
* Ref처럼 정렬된 장수 목록에서 국가가 처음 나타난 순서대로 묶는다.
|
||||
* 머리 글자색은 Ref `isBrightColor()` 기준이다.
|
||||
*/
|
||||
export const groupGeneralOptionsByNation = (
|
||||
options: readonly CommandOption[],
|
||||
nations: readonly CommandOption[]
|
||||
): GeneralOptionGroup[] => {
|
||||
const nationById = new Map(nations.map((nation) => [nation.value, nation]));
|
||||
const groups = new Map<number, GeneralOptionGroup>();
|
||||
for (const option of options) {
|
||||
const nationId = option.nationId ?? 0;
|
||||
let group = groups.get(nationId);
|
||||
if (!group) {
|
||||
const nation = nationId === 0 ? undefined : nationById.get(nationId);
|
||||
const name =
|
||||
nationId === 0
|
||||
? WANDERER.name
|
||||
: (nation?.targetNames?.name ?? nation?.label ?? option.targetNames?.nationName ?? WANDERER.name);
|
||||
const color = nationId === 0 ? WANDERER.color : (nation?.color ?? WANDERER.color);
|
||||
group = {
|
||||
nationId,
|
||||
name,
|
||||
color,
|
||||
textColor: legacyLuminanceTextColor(color),
|
||||
options: [],
|
||||
};
|
||||
groups.set(nationId, group);
|
||||
}
|
||||
group.options.push(option);
|
||||
}
|
||||
return [...groups.values()];
|
||||
};
|
||||
@@ -17,6 +17,7 @@ export type CommandOption = {
|
||||
crew?: number;
|
||||
troopId?: number;
|
||||
npcState?: number;
|
||||
nationId?: number;
|
||||
};
|
||||
|
||||
export type CommandAmountPreset = {
|
||||
|
||||
@@ -12,6 +12,11 @@ import { commandArgumentPresentation, resolveCommandArgumentMapTarget } from '..
|
||||
import { commandCityOptions } from '../command/commandArgumentOptions';
|
||||
import { COMMAND_CITY_DISTANCE_RANGE, citiesBasedOnDistance } from '../command/commandCityDistance';
|
||||
import { sortCommandGeneralOptions } from '../command/commandGeneralOptions';
|
||||
import {
|
||||
groupGeneralOptionsByNation,
|
||||
NATION_GROUPED_GENERAL_COMMANDS,
|
||||
type GeneralOptionGroup,
|
||||
} from '../command/commandGeneralGroups';
|
||||
import {
|
||||
commandArgumentFieldContract,
|
||||
shouldPreserveCommandArgumentValue,
|
||||
@@ -141,6 +146,18 @@ const filteredTargets = computed(
|
||||
);
|
||||
const visibleOptionsFor = (field: CommandInputField): CommandOption[] =>
|
||||
filteredTargets.value.get(field.key) ?? optionsFor(field);
|
||||
// 등용·장수대상임관은 Ref SelectGeneral groupByNation처럼 국가색 머리로 후보를 묶는다.
|
||||
const isNationGroupedField = (field: CommandInputField): boolean =>
|
||||
field.optionSource === 'generals' && NATION_GROUPED_GENERAL_COMMANDS.has(props.commandKey);
|
||||
const nationGroupsOf = (field: CommandInputField, options: CommandOption[]): GeneralOptionGroup[] | null =>
|
||||
isNationGroupedField(field) ? groupGeneralOptionsByNation(options, props.options.nations) : null;
|
||||
type TargetCardGroup = { key: string; header: GeneralOptionGroup | null; options: CommandOption[] };
|
||||
const targetCardGroupsFor = (field: CommandInputField): TargetCardGroup[] => {
|
||||
const options = visibleOptionsFor(field);
|
||||
const groups = nationGroupsOf(field, options);
|
||||
if (!groups) return [{ key: 'all', header: null, options }];
|
||||
return groups.map((group) => ({ key: `nation-${group.nationId}`, header: group, options: group.options }));
|
||||
};
|
||||
const clearSearchQueries = () => {
|
||||
for (const key of searchTimers.keys()) cancelSearchTimer(key);
|
||||
for (const key of Object.keys(searchDrafts)) delete searchDrafts[key];
|
||||
@@ -574,14 +591,32 @@ watch(
|
||||
:style="colorOptionStyle(field, selectedOptionFor(field))"
|
||||
@change="setSelectValue(field, ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in optionsFor(field)"
|
||||
:key="String(option.value)"
|
||||
:value="String(option.value)"
|
||||
:style="colorOptionStyle(field, option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
<template v-if="isNationGroupedField(field)">
|
||||
<optgroup
|
||||
v-for="group in nationGroupsOf(field, optionsFor(field))"
|
||||
:key="group.nationId"
|
||||
:label="group.name"
|
||||
>
|
||||
<option
|
||||
v-for="option in group.options"
|
||||
:key="String(option.value)"
|
||||
:value="String(option.value)"
|
||||
:style="colorOptionStyle(field, option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
<template v-else>
|
||||
<option
|
||||
v-for="option in optionsFor(field)"
|
||||
:key="String(option.value)"
|
||||
:value="String(option.value)"
|
||||
:style="colorOptionStyle(field, option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
<div v-else-if="field.kind === 'boolean'" class="boolean-options">
|
||||
<button
|
||||
@@ -718,25 +753,37 @@ watch(
|
||||
<span v-if="!visibleOptionsFor(field).length" class="target-search-empty"
|
||||
>검색 결과가 없습니다.</span
|
||||
>
|
||||
<button
|
||||
v-for="option in visibleOptionsFor(field)"
|
||||
:key="String(option.value)"
|
||||
type="button"
|
||||
class="target-option"
|
||||
:class="{
|
||||
selected: option.value === values[field.key],
|
||||
unavailable: option.availableNow === false,
|
||||
}"
|
||||
:aria-pressed="option.value === values[field.key]"
|
||||
@click="setSelectValue(field, String(option.value))"
|
||||
>
|
||||
<span v-if="option.color" class="option-color" :style="{ backgroundColor: option.color }" />
|
||||
<strong :style="colorOptionStyle(field, option)">{{ option.label }}</strong>
|
||||
<span class="target-state">{{
|
||||
option.availableNow === false ? '현재 불가' : option.availableNow ? '우선 대상' : '대상'
|
||||
}}</span>
|
||||
<small>{{ commandTargetDescription(commandKey, option) }}</small>
|
||||
</button>
|
||||
<template v-for="group in targetCardGroupsFor(field)" :key="group.key">
|
||||
<div
|
||||
v-if="group.header"
|
||||
class="target-group-header"
|
||||
data-testid="general-nation-group"
|
||||
:style="{ backgroundColor: group.header.color, color: group.header.textColor }"
|
||||
>
|
||||
{{ group.header.name }}
|
||||
</div>
|
||||
<button
|
||||
v-for="option in group.options"
|
||||
:key="String(option.value)"
|
||||
type="button"
|
||||
class="target-option"
|
||||
:class="{
|
||||
selected: option.value === values[field.key],
|
||||
unavailable: option.availableNow === false,
|
||||
}"
|
||||
:aria-pressed="option.value === values[field.key]"
|
||||
@click="setSelectValue(field, String(option.value))"
|
||||
>
|
||||
<span v-if="option.color" class="option-color" :style="{ backgroundColor: option.color }" />
|
||||
<strong :style="colorOptionStyle(field, option)">{{
|
||||
group.header ? (option.targetNames?.name ?? option.label) : option.label
|
||||
}}</strong>
|
||||
<span class="target-state">{{
|
||||
option.availableNow === false ? '현재 불가' : option.availableNow ? '우선 대상' : '대상'
|
||||
}}</span>
|
||||
<small>{{ commandTargetDescription(commandKey, option) }}</small>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isValid" class="argument-error" role="alert">필수 입력을 확인하세요.</div>
|
||||
@@ -980,6 +1027,14 @@ small {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.target-group-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding: 4px 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.target-state {
|
||||
grid-column: 3;
|
||||
color: #aee6a7;
|
||||
|
||||
@@ -4,7 +4,13 @@ export const commandTargetDescription = (commandKey: string, option?: CommandOpt
|
||||
if (!option) return '';
|
||||
const description = option.description ?? '';
|
||||
// 이전 API는 완성된 description을 주므로 새 원본 필드가 없으면 그대로 표시한다.
|
||||
if (option.targetNames?.troopName === undefined || commandKey === 'che_포상' || commandKey === 'che_몰수') {
|
||||
// Ref ProcessGeneralAmount·che_선양.vue는 부대를 표시하지 않는다.
|
||||
if (
|
||||
option.targetNames?.troopName === undefined ||
|
||||
commandKey === 'che_포상' ||
|
||||
commandKey === 'che_몰수' ||
|
||||
commandKey === 'che_선양'
|
||||
) {
|
||||
return description;
|
||||
}
|
||||
const troopName = option.targetNames.troopName ?? (option.troopId ? `#${option.troopId}` : '없음');
|
||||
|
||||
Reference in New Issue
Block a user