281 lines
13 KiB
TypeScript
281 lines
13 KiB
TypeScript
import { DIPLOMACY_STATE } from '@sammo-ts/logic';
|
|
|
|
import type { TurnCommandAmountPreset, TurnCommandOption } from './commandInput.js';
|
|
|
|
export interface GeneralTargetSource {
|
|
id: number;
|
|
name: string;
|
|
nationId: number;
|
|
cityId: number;
|
|
npcState: number;
|
|
officerLevel: number;
|
|
gold?: number;
|
|
rice?: number;
|
|
leadership?: number;
|
|
strength?: number;
|
|
intel?: number;
|
|
crew?: number;
|
|
train?: number;
|
|
atmos?: number;
|
|
troopId?: number;
|
|
}
|
|
|
|
export interface NationTargetSource {
|
|
id: number;
|
|
name: string;
|
|
color: string;
|
|
capitalName?: string;
|
|
level: number;
|
|
power: number;
|
|
generalCount: number;
|
|
cityCount: number;
|
|
diplomacyState: number;
|
|
diplomacyTerm: number;
|
|
adjacent: boolean;
|
|
diplomacyRestricted?: boolean;
|
|
}
|
|
|
|
export interface RefGeneralTargetOptions {
|
|
generals: TurnCommandOption[];
|
|
generalTargets: Record<string, TurnCommandOption[]>;
|
|
}
|
|
|
|
export interface RefNationTargetOptions {
|
|
nations: TurnCommandOption[];
|
|
nationTargets: Record<string, TurnCommandOption[]>;
|
|
}
|
|
|
|
const SAME_NATION_GENERAL_COMMANDS = ['che_증여'] as const;
|
|
const SAME_NATION_NATION_COMMANDS = ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'] as const;
|
|
|
|
/** Ref 각 처리 화면의 SELECT 조건을 공통 command table의 명령별 option으로 투영한다. */
|
|
export const buildRefGeneralTargetOptions = (options: {
|
|
actorId: number;
|
|
actorNationId: number;
|
|
generals: readonly GeneralTargetSource[];
|
|
nationNames: ReadonlyMap<number, string>;
|
|
cityNames: ReadonlyMap<number, string>;
|
|
troopNames?: ReadonlyMap<number, string>;
|
|
}): RefGeneralTargetOptions => {
|
|
const toOption = (entry: GeneralTargetSource, action?: string): TurnCommandOption => {
|
|
const troopName = entry.troopId ? options.troopNames?.get(entry.troopId) : undefined;
|
|
const cityName = options.cityNames.get(entry.cityId) ?? '재야';
|
|
const isTroopMember = Boolean(entry.troopId && entry.troopId !== entry.id);
|
|
const isTroopExit = action === 'che_부대탈퇴지시';
|
|
const availableNow = isTroopExit ? isTroopMember && entry.id !== options.actorId : undefined;
|
|
const label = (() => {
|
|
if (action === 'che_발령') return `${entry.name} (${cityName})`;
|
|
if (action === 'che_포상' || action === 'che_몰수') return `${entry.name} (${cityName})`;
|
|
return `${entry.name} (${options.nationNames.get(entry.nationId) ?? '무소속'} · ${cityName})`;
|
|
})();
|
|
const details = [
|
|
entry.gold === undefined ? null : `금 ${entry.gold.toLocaleString()}`,
|
|
entry.rice === undefined ? null : `쌀 ${entry.rice.toLocaleString()}`,
|
|
entry.crew === undefined ? null : `병력 ${entry.crew.toLocaleString()}`,
|
|
entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`,
|
|
entry.atmos === undefined ? null : `사기 ${entry.atmos.toLocaleString()}`,
|
|
].filter((value): value is string => Boolean(value));
|
|
// 발령 후보는 Ref처럼 능력치와 병력 준비 상태를 함께 비교한다.
|
|
// 예약 요약에 쓰는 label은 그대로 두고, 같은 국가 후보의 상세 정보만 보강한다.
|
|
const assignmentDetails =
|
|
action === 'che_발령'
|
|
? [
|
|
[
|
|
entry.leadership === undefined ? null : `통솔 ${entry.leadership.toLocaleString()}`,
|
|
entry.strength === undefined ? null : `무력 ${entry.strength.toLocaleString()}`,
|
|
entry.intel === undefined ? null : `지력 ${entry.intel.toLocaleString()}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
[
|
|
entry.crew === undefined ? null : `병력 ${entry.crew.toLocaleString()}`,
|
|
entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`,
|
|
entry.atmos === undefined ? null : `사기 ${entry.atmos.toLocaleString()}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
[
|
|
entry.gold === undefined ? null : `금 ${entry.gold.toLocaleString()}`,
|
|
entry.rice === undefined ? null : `쌀 ${entry.rice.toLocaleString()}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
: undefined;
|
|
if (isTroopExit) {
|
|
details.unshift(availableNow ? '현재 탈퇴 지시 가능' : '현재 탈퇴 지시 불가');
|
|
}
|
|
return {
|
|
value: entry.id,
|
|
label,
|
|
targetNames: {
|
|
name: entry.name,
|
|
nationName: options.nationNames.get(entry.nationId) ?? null,
|
|
cityName: options.cityNames.get(entry.cityId) ?? null,
|
|
troopName: troopName ?? null,
|
|
},
|
|
description: assignmentDetails ?? details.join(' · '),
|
|
...(availableNow === undefined ? {} : { availableNow }),
|
|
...(entry.gold === undefined ? {} : { gold: entry.gold }),
|
|
...(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[] =>
|
|
options.generals.filter(predicate).map((entry) => toOption(entry));
|
|
|
|
const generalTargets: Record<string, TurnCommandOption[]> = {};
|
|
for (const action of SAME_NATION_GENERAL_COMMANDS) {
|
|
generalTargets[action] = options.generals
|
|
.filter((entry) => entry.nationId === options.actorNationId)
|
|
.map((entry) => toOption(entry, action));
|
|
}
|
|
for (const action of SAME_NATION_NATION_COMMANDS) {
|
|
generalTargets[action] = options.generals
|
|
.filter((entry) => entry.nationId === options.actorNationId)
|
|
.map((entry) => toOption(entry, action))
|
|
.sort((left, right) => Number(right.availableNow) - Number(left.availableNow));
|
|
}
|
|
|
|
generalTargets.che_선양 = project(
|
|
(entry) => entry.nationId !== 0 && entry.nationId === options.actorNationId && entry.id !== options.actorId
|
|
);
|
|
generalTargets.che_등용 = project(
|
|
(entry) => entry.npcState < 2 && entry.officerLevel !== 12 && entry.id !== options.actorId
|
|
);
|
|
generalTargets.che_장수대상임관 = project((entry) => entry.id !== options.actorId);
|
|
|
|
return {
|
|
// 기존 profile의 공통 fallback은 유저장 목록을 유지한다.
|
|
generals: project((entry) => entry.npcState < 2),
|
|
generalTargets,
|
|
};
|
|
};
|
|
|
|
const DIPLOMACY_LABELS: Record<number, string> = {
|
|
[DIPLOMACY_STATE.WAR]: '전쟁',
|
|
[DIPLOMACY_STATE.DECLARATION]: '선포',
|
|
[DIPLOMACY_STATE.TRADE]: '교역',
|
|
[DIPLOMACY_STATE.NON_AGGRESSION]: '불가침',
|
|
};
|
|
|
|
const NATION_TARGET_COMMANDS = [
|
|
'che_물자원조',
|
|
'che_불가침제의',
|
|
'che_선전포고',
|
|
'che_종전제의',
|
|
'che_불가침파기제의',
|
|
] as const;
|
|
|
|
const nationAvailability = (
|
|
action: (typeof NATION_TARGET_COMMANDS)[number],
|
|
actorNationId: number,
|
|
target: NationTargetSource
|
|
): { available: boolean; reason: string } => {
|
|
if (target.id === actorNationId) return { available: false, reason: '아국은 대상이 아닙니다.' };
|
|
if (action === 'che_물자원조') {
|
|
return target.diplomacyRestricted
|
|
? { available: false, reason: '상대국이 외교제한 중입니다.' }
|
|
: { available: true, reason: '현재 원조 대상' };
|
|
}
|
|
if (action === 'che_불가침제의') {
|
|
const available = ![DIPLOMACY_STATE.WAR, DIPLOMACY_STATE.DECLARATION].includes(target.diplomacyState as 0 | 1);
|
|
return { available, reason: available ? '현재 제의 가능' : '교전·선포 중에는 제의 불가' };
|
|
}
|
|
if (action === 'che_선전포고') {
|
|
if (!target.adjacent) return { available: false, reason: '인접 국가가 아닙니다.' };
|
|
const available = ![DIPLOMACY_STATE.WAR, DIPLOMACY_STATE.DECLARATION, DIPLOMACY_STATE.NON_AGGRESSION].includes(
|
|
target.diplomacyState as 0 | 1 | 7
|
|
);
|
|
return { available, reason: available ? '현재 선전포고 가능' : '현재 외교 관계에서는 선전포고 불가' };
|
|
}
|
|
if (action === 'che_종전제의') {
|
|
const available = [DIPLOMACY_STATE.WAR, DIPLOMACY_STATE.DECLARATION].includes(target.diplomacyState as 0 | 1);
|
|
return { available, reason: available ? '현재 종전 제의 가능' : '전쟁·선포 중인 국가가 아닙니다.' };
|
|
}
|
|
const available = target.diplomacyState === DIPLOMACY_STATE.NON_AGGRESSION;
|
|
return { available, reason: available ? '현재 불가침 파기 제의 가능' : '불가침 중인 국가가 아닙니다.' };
|
|
};
|
|
|
|
/** 사령턴 외교 대상은 현재 명령에 맞는 국가부터 보이되, 예약 자체는 모든 대상을 유지한다. */
|
|
export const buildRefNationTargetOptions = (options: {
|
|
actorNationId: number;
|
|
nations: readonly NationTargetSource[];
|
|
}): RefNationTargetOptions => {
|
|
const baseOptions = options.nations.map<TurnCommandOption>((entry) => ({
|
|
value: entry.id,
|
|
label: entry.name,
|
|
targetNames: { name: entry.name, capitalName: entry.capitalName ?? null },
|
|
color: entry.color,
|
|
description: `수도 ${entry.capitalName ?? '-'} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
|
|
}));
|
|
const nationTargets: Record<string, TurnCommandOption[]> = {};
|
|
for (const action of NATION_TARGET_COMMANDS) {
|
|
nationTargets[action] = options.nations
|
|
.map((entry) => {
|
|
const availability = nationAvailability(action, options.actorNationId, entry);
|
|
const relation = DIPLOMACY_LABELS[entry.diplomacyState] ?? `관계 ${entry.diplomacyState}`;
|
|
const term = entry.diplomacyTerm > 0 ? ` ${entry.diplomacyTerm}턴` : '';
|
|
return {
|
|
value: entry.id,
|
|
label: entry.name,
|
|
targetNames: { name: entry.name, capitalName: entry.capitalName ?? null },
|
|
color: entry.color,
|
|
availableNow: availability.available,
|
|
description: `${availability.reason} · ${relation}${term} · 수도 ${entry.capitalName ?? '-'} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
|
|
power: entry.power,
|
|
} as TurnCommandOption & { power: number };
|
|
})
|
|
.sort(
|
|
(left, right) =>
|
|
Number(right.availableNow) - Number(left.availableNow) ||
|
|
right.power - left.power ||
|
|
Number(left.value) - Number(right.value)
|
|
)
|
|
.map(({ power: _power, ...entry }) => entry);
|
|
}
|
|
return { nations: baseOptions, nationTargets };
|
|
};
|
|
|
|
const RESOURCE_ACTION_GUIDE = [
|
|
100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200, 1500, 2000, 2500, 3000, 4000, 5000, 6000, 7000, 8000, 9000,
|
|
10000,
|
|
];
|
|
|
|
/** Ref SelectAmount의 dropdown 값을 공통 예약 입력 DTO로 옮긴다. */
|
|
export const buildRefAmountPresets = (
|
|
nationLevel: number,
|
|
maxResourceActionAmount: number
|
|
): Record<string, TurnCommandAmountPreset> => {
|
|
const resourceMax = maxResourceActionAmount > 0 ? maxResourceActionAmount : 10_000;
|
|
const resourceValues = RESOURCE_ACTION_GUIDE.filter((value) => value <= resourceMax);
|
|
if (!resourceValues.includes(resourceMax)) resourceValues.push(resourceMax);
|
|
const resourcePreset: TurnCommandAmountPreset = {
|
|
values: resourceValues,
|
|
defaultValue: Math.min(1000, resourceMax),
|
|
min: Math.min(100, resourceMax),
|
|
max: resourceMax,
|
|
step: 1,
|
|
};
|
|
const aidMax = Math.max(10_000, Math.max(1, nationLevel) * 10_000);
|
|
const aidPreset: TurnCommandAmountPreset = {
|
|
values: Array.from({ length: Math.max(1, nationLevel) }, (_, index) => (index + 1) * 10_000),
|
|
defaultValue: Math.min(1000, aidMax),
|
|
min: 1000,
|
|
max: aidMax,
|
|
step: 10,
|
|
};
|
|
return {
|
|
che_증여: resourcePreset,
|
|
che_헌납: resourcePreset,
|
|
che_군량매매: resourcePreset,
|
|
che_포상: resourcePreset,
|
|
che_몰수: resourcePreset,
|
|
che_물자원조: aidPreset,
|
|
};
|
|
};
|