장비매매 불가 표시·숙련전환 전후 미리보기·건국 성향표를 Ref처럼 복원
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,7 +78,7 @@ const PRESENTATIONS: Record<string, CommandArgumentPresentation> = {
|
||||
che_숙련전환: {
|
||||
lines: ['선택한 병과 숙련을 40% 줄이고, 줄어든 숙련의 90%를 다른 병과 숙련으로 전환합니다.'],
|
||||
},
|
||||
che_장비매매: { lines: ['장비를 구입하거나 매각합니다.', '가격과 요구 치안, 장비 효과를 확인한 뒤 선택하세요.'] },
|
||||
che_장비매매: { lines: ['장비를 구입하거나 매각합니다.', '현재 구입 불가능한 것은 붉은색으로 표시됩니다.'] },
|
||||
che_건국: {
|
||||
lines: ['현재 중·소도시에서 나라를 세웁니다.', '국가 성향별 장단점을 확인한 뒤 국명과 색상을 정하세요.'],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { dexProgress } from '../../utils/legacyProgress.ts';
|
||||
import type { CommandOption } from './types';
|
||||
|
||||
/** Ref che_장비매매.vue: 현재 구입(판매) 불가능한 항목은 붉은색 `(불가)`로 표시한다. */
|
||||
export const equipmentOptionText = (option: CommandOption): string =>
|
||||
option.availableNow === false ? `${option.label} (불가)` : option.label;
|
||||
|
||||
export type DexAmountInfo = { amount: number; name: string; color: string };
|
||||
export type DexConversionRow = { armName: string; before: DexAmountInfo; after: DexAmountInfo };
|
||||
|
||||
/** Core che_숙련전환 실행 계수. Ref che_숙련전환 $decreaseCoeff·$convertCoeff와 같다. */
|
||||
export const DEX_DECREASE_COEFF = 0.4;
|
||||
export const DEX_CONVERT_COEFF = 0.9;
|
||||
|
||||
const dexAmountInfo = (amount: number): DexAmountInfo => {
|
||||
const progress = dexProgress(amount);
|
||||
return { amount, name: progress.name, color: progress.color };
|
||||
};
|
||||
|
||||
export const dexGradeName = (amount: number | undefined): string | undefined =>
|
||||
amount === undefined ? undefined : dexProgress(amount).name;
|
||||
|
||||
/**
|
||||
* Ref che_숙련전환.vue의 감소 대상·전환 대상 전/후 두 줄.
|
||||
* Ref 화면은 소수 그대로 계산한 뒤 내림해 보이지만, 여기서는 실제 실행과 같은 정수 절삭
|
||||
* (`cut = trunc(src × 0.4)`, `add = trunc(cut × 0.9)`) 값을 보인다.
|
||||
* 같은 병과끼리는 예약할 수 없으므로 미리보기를 만들지 않는다.
|
||||
*/
|
||||
export const dexConversionPreview = (
|
||||
srcArmType: unknown,
|
||||
destArmType: unknown,
|
||||
armTypes: readonly CommandOption[],
|
||||
dexterity: Readonly<Record<string, number>> | undefined
|
||||
): DexConversionRow[] | null => {
|
||||
if (!dexterity || srcArmType === destArmType) return null;
|
||||
const src = armTypes.find((entry) => entry.value === srcArmType);
|
||||
const dest = armTypes.find((entry) => entry.value === destArmType);
|
||||
if (!src || !dest) return null;
|
||||
const srcDex = dexterity[String(src.value)] ?? 0;
|
||||
const destDex = dexterity[String(dest.value)] ?? 0;
|
||||
const cutDex = Math.trunc(srcDex * DEX_DECREASE_COEFF);
|
||||
const addDex = Math.trunc(cutDex * DEX_CONVERT_COEFF);
|
||||
return [
|
||||
{ armName: src.label, before: dexAmountInfo(srcDex), after: dexAmountInfo(srcDex - cutDex) },
|
||||
{ armName: dest.label, before: dexAmountInfo(destDex), after: dexAmountInfo(destDex + addDex) },
|
||||
];
|
||||
};
|
||||
|
||||
export type NationTypeRow = { value: CommandOption['value']; name: string; pros: string; cons: string };
|
||||
|
||||
/**
|
||||
* Ref che_건국.vue 성향표: `- 이름 : 장점, 단점`.
|
||||
* Core 국가 성향 info는 Ref `$pros`와 `$cons`를 공백으로 이은 문자열이므로 ↑/↓ 항목으로 다시 나눈다.
|
||||
*/
|
||||
export const nationTypeRows = (options: readonly CommandOption[]): NationTypeRow[] =>
|
||||
options.map((option) => {
|
||||
const tokens = (option.description ?? '').split(/\s+/).filter(Boolean);
|
||||
return {
|
||||
value: option.value,
|
||||
name: option.label,
|
||||
pros: tokens.filter((token) => token.endsWith('↑')).join(' '),
|
||||
cons: tokens.filter((token) => !token.endsWith('↑')).join(' '),
|
||||
};
|
||||
});
|
||||
@@ -61,6 +61,8 @@ export type CommandInputContext = {
|
||||
nationGold?: number;
|
||||
nationRice?: number;
|
||||
nationLevel?: number;
|
||||
/** 숙련전환 전/후 미리보기용 본인 병과별 숙련. */
|
||||
dexterity?: Record<string, number>;
|
||||
};
|
||||
|
||||
export type CommandInputField = {
|
||||
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
scoutMessageRows,
|
||||
type ScoutMessageRow,
|
||||
} from '../command/commandNationDetails';
|
||||
import {
|
||||
dexConversionPreview,
|
||||
dexGradeName,
|
||||
equipmentOptionText,
|
||||
nationTypeRows,
|
||||
} from '../command/commandPersonalDetails';
|
||||
import {
|
||||
commandArgumentFieldContract,
|
||||
shouldPreserveCommandArgumentValue,
|
||||
@@ -259,14 +265,41 @@ const colorOptionStyle = (field: CommandInputField, option?: CommandOption): CSS
|
||||
if (isCounterStrategyField(field) && option && strategyOption(option).remainTurn > 0) {
|
||||
return { color: 'red' };
|
||||
}
|
||||
if (field.optionSource === 'items' && option?.availableNow === false) {
|
||||
return { color: 'red' };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isCounterStrategyField = (field: CommandInputField): boolean =>
|
||||
props.commandKey === 'che_피장파장' && field.key === 'commandType';
|
||||
const strategyOption = (option: CommandOption) => counterStrategyOption(option, props.options.strategyCooldowns);
|
||||
const optionText = (field: CommandInputField, option: CommandOption): string =>
|
||||
isCounterStrategyField(field) ? strategyOption(option).label : option.label;
|
||||
const isDexConversionField = (field: CommandInputField): boolean =>
|
||||
props.commandKey === 'che_숙련전환' && field.optionSource === 'armTypes';
|
||||
const optionText = (field: CommandInputField, option: CommandOption): string => {
|
||||
if (isCounterStrategyField(field)) return strategyOption(option).label;
|
||||
if (field.optionSource === 'items') return equipmentOptionText(option);
|
||||
if (isDexConversionField(field)) {
|
||||
// Ref che_숙련전환.vue: `병과 (등급)`.
|
||||
const grade = dexGradeName(props.options.context?.dexterity?.[String(option.value)]);
|
||||
return grade ? `${option.label} (${grade})` : option.label;
|
||||
}
|
||||
return option.label;
|
||||
};
|
||||
const dexPreview = computed(() =>
|
||||
props.commandKey === 'che_숙련전환'
|
||||
? dexConversionPreview(
|
||||
values.srcArmType,
|
||||
values.destArmType,
|
||||
props.options.armTypes,
|
||||
props.options.context?.dexterity
|
||||
)
|
||||
: null
|
||||
);
|
||||
const nationTypeTable = computed(() =>
|
||||
props.fields.some((field) => field.optionSource === 'nationTypes') ? nationTypeRows(props.options.nationTypes) : []
|
||||
);
|
||||
const nationTypeField = computed(() => props.fields.find((field) => field.optionSource === 'nationTypes'));
|
||||
|
||||
const cityTargetField = computed(() =>
|
||||
props.fields.find(
|
||||
@@ -584,6 +617,24 @@ watch(
|
||||
>: {{ row.amount.toLocaleString() }}
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="nationTypeTable.length" class="nation-type-list" data-testid="nation-type-list">
|
||||
<li
|
||||
v-for="row in nationTypeTable"
|
||||
:key="String(row.value)"
|
||||
:class="{ selected: nationTypeField && row.value === values[nationTypeField.key] }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="nation-type-name"
|
||||
:aria-pressed="Boolean(nationTypeField && row.value === values[nationTypeField.key])"
|
||||
@click="nationTypeField && setSelectValue(nationTypeField, String(row.value))"
|
||||
>
|
||||
- {{ row.name }}
|
||||
</button>
|
||||
<span>: <span class="nation-type-pros">{{ row.pros }}</span>,</span>
|
||||
<span class="nation-type-cons">{{ row.cons }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-for="field in visibleFields" :key="field.key" class="argument-row">
|
||||
<label :for="`command-arg-${field.key}`">{{ field.label }}</label>
|
||||
<input
|
||||
@@ -836,6 +887,27 @@ watch(
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dexPreview" class="dex-conversion-preview" data-testid="dex-conversion-preview">
|
||||
<div v-for="row in dexPreview" :key="row.armName" class="dex-conversion-row">
|
||||
<div>{{ row.armName }}</div>
|
||||
<div class="dex-bracket-open">[</div>
|
||||
<div :style="{ color: row.before.color }">{{ row.before.name }}</div>
|
||||
<div class="dex-amount">{{ row.before.amount.toLocaleString() }}</div>
|
||||
<div>]</div>
|
||||
<div class="dex-arrow">→</div>
|
||||
<div class="dex-bracket-open">[</div>
|
||||
<div :style="{ color: row.after.color }">{{ row.after.name }}</div>
|
||||
<div class="dex-amount">{{ row.after.amount.toLocaleString() }}</div>
|
||||
<div>]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="commandKey === 'che_숙련전환' && values.srcArmType === values.destArmType"
|
||||
class="dex-conversion-same"
|
||||
data-testid="dex-conversion-same"
|
||||
>
|
||||
감소 대상과 전환 대상 숙련이 같습니다. 다른 병과를 고르세요.
|
||||
</div>
|
||||
<div v-if="!isValid" class="argument-error" role="alert">필수 입력을 확인하세요.</div>
|
||||
</div>
|
||||
<section
|
||||
@@ -974,6 +1046,100 @@ small {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Ref che_건국.vue 성향표: col-2/col-4/col-4, lg 이상 col-lg-1/col-lg-2/col-lg-2. */
|
||||
.nation-type-list {
|
||||
margin: 0;
|
||||
padding: 4px 0 6px 2rem;
|
||||
color: #e8ddc4;
|
||||
font-size: var(--sammo-font-size-normal);
|
||||
line-height: 1.45;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nation-type-list li {
|
||||
display: grid;
|
||||
grid-template-columns: calc(100% / 6) calc(100% / 3) calc(100% / 3);
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.nation-type-list li {
|
||||
grid-template-columns: calc(100% / 12) calc(100% / 6) calc(100% / 6);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ref 최소 폭 500px보다 좁으면 비율 칸이 장단점을 줄바꿈하므로 내용 폭 열로 바꾼다(Core 보조). */
|
||||
@media (max-width: 499px) {
|
||||
.nation-type-list {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.nation-type-list li {
|
||||
grid-template-columns: max-content max-content minmax(0, 1fr);
|
||||
column-gap: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.nation-type-list li.selected {
|
||||
background: rgba(201, 164, 90, 0.18);
|
||||
}
|
||||
|
||||
.nation-type-name {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nation-type-list li.selected .nation-type-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nation-type-pros {
|
||||
color: cyan;
|
||||
}
|
||||
|
||||
.nation-type-cons {
|
||||
color: magenta;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ref che_숙련전환.vue 전/후 grid(3ch 1ch 2ch 10ch 1ch 3ch 1ch 2ch 10ch 1ch).
|
||||
* 한글 병과명과 EX 등급이 옆 칸을 덮지 않도록 병과명은 4em, 등급은 3ch로 넓힌다.
|
||||
*/
|
||||
.dex-conversion-preview {
|
||||
padding: 4px 8px;
|
||||
color: #e8ddc4;
|
||||
font-size: var(--sammo-font-size-normal);
|
||||
}
|
||||
|
||||
.dex-conversion-row {
|
||||
display: grid;
|
||||
grid-template-columns: 4em 1ch 3ch 10ch 1ch 3ch 1ch 3ch 10ch 1ch;
|
||||
}
|
||||
|
||||
.dex-bracket-open,
|
||||
.dex-amount {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.dex-amount {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dex-arrow {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dex-conversion-same {
|
||||
padding: 4px 8px;
|
||||
color: #ffb4a8;
|
||||
font-size: var(--sammo-font-size-normal);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ref che_임관.vue nation-list. 940px 이상은 130px 국가명 + 870px 권유문, 그 아래는 한 열이다.
|
||||
* 지도·입력 2열 아래에 전체 폭으로 둔다.
|
||||
|
||||
Reference in New Issue
Block a user