daemon 진행 뒤 국가 명령을 현재 슬롯에 재기준화하고 Ref 명령별 장수 대상 필터를 적용한다. 모바일 고급 편집기 12턴과 500px 8수뇌 행렬을 실제 Chromium 기준으로 복원한다.
312 lines
8.8 KiB
TypeScript
312 lines
8.8 KiB
TypeScript
import {
|
|
loadGeneralTurnCommandSpecs,
|
|
loadNationTurnCommandSpecs,
|
|
type GeneralTurnCommandSpec,
|
|
type NationTurnCommandSpec,
|
|
} from '@sammo-ts/logic';
|
|
import { asRecord, isRecord } from '@sammo-ts/common';
|
|
import { z } from 'zod';
|
|
|
|
import { loadTurnCommandProfile } from '@sammo-ts/game-engine/turn/turnCommandProfile.js';
|
|
|
|
export type TurnCommandOptionValue = string | number;
|
|
|
|
export interface TurnCommandOption {
|
|
value: TurnCommandOptionValue;
|
|
label: string;
|
|
color?: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface TurnCommandRecruitmentCrewType {
|
|
id: number;
|
|
armType: number;
|
|
name: string;
|
|
available: boolean;
|
|
special: boolean;
|
|
attack: number;
|
|
defence: number;
|
|
speed: number;
|
|
avoid: number;
|
|
baseCost: number;
|
|
baseRice: number;
|
|
info: string[];
|
|
}
|
|
|
|
export interface TurnCommandRecruitmentGroup {
|
|
armType: number;
|
|
armName: string;
|
|
values: TurnCommandRecruitmentCrewType[];
|
|
}
|
|
|
|
export interface TurnCommandRecruitmentInfo {
|
|
techLevel: number;
|
|
leadership: number;
|
|
fullLeadership: number;
|
|
currentCrewTypeId: number;
|
|
currentCrewTypeName: string;
|
|
crew: number;
|
|
gold: number;
|
|
groups: TurnCommandRecruitmentGroup[];
|
|
}
|
|
|
|
export type TurnCommandOptionSource =
|
|
'cities' | 'nations' | 'generals' | 'crewTypes' | 'armTypes' | 'nationTypes' | 'colors' | 'items';
|
|
|
|
export interface TurnCommandInputField {
|
|
key: string;
|
|
label: string;
|
|
kind: 'text' | 'number' | 'boolean' | 'select' | 'numberTuple' | 'hidden';
|
|
required: boolean;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
constValue?: TurnCommandOptionValue;
|
|
options?: TurnCommandOption[];
|
|
optionSource?: TurnCommandOptionSource;
|
|
tupleLabels?: string[];
|
|
}
|
|
|
|
export interface TurnCommandInputOptions {
|
|
cities: TurnCommandOption[];
|
|
nations: TurnCommandOption[];
|
|
generals: TurnCommandOption[];
|
|
generalTargets?: Record<string, TurnCommandOption[]>;
|
|
crewTypes: TurnCommandOption[];
|
|
armTypes: TurnCommandOption[];
|
|
nationTypes: TurnCommandOption[];
|
|
colors: TurnCommandOption[];
|
|
items: Record<string, TurnCommandOption[]>;
|
|
recruitment: TurnCommandRecruitmentInfo | null;
|
|
context?: {
|
|
actorGold: number;
|
|
actorRice: number;
|
|
citySecurity?: number;
|
|
nationGold?: number;
|
|
nationRice?: number;
|
|
nationLevel?: number;
|
|
};
|
|
}
|
|
|
|
// 레거시 및 명령 실행 모듈의 인덱스 순서와 동일해야 한다.
|
|
export const TURN_COMMAND_NATION_COLORS = [
|
|
'#FF0000',
|
|
'#800000',
|
|
'#A0522D',
|
|
'#FF6347',
|
|
'#FFA500',
|
|
'#FFDAB9',
|
|
'#FFD700',
|
|
'#FFFF00',
|
|
'#7CFC00',
|
|
'#00FF00',
|
|
'#808000',
|
|
'#008000',
|
|
'#2E8B57',
|
|
'#008080',
|
|
'#20B2AA',
|
|
'#6495ED',
|
|
'#7FFFD4',
|
|
'#AFEEEE',
|
|
'#87CEEB',
|
|
'#00FFFF',
|
|
'#00BFFF',
|
|
'#0000FF',
|
|
'#000080',
|
|
'#483D8B',
|
|
'#7B68EE',
|
|
'#BA55D3',
|
|
'#800080',
|
|
'#FF00FF',
|
|
'#FFC0CB',
|
|
'#F5F5DC',
|
|
'#E0FFFF',
|
|
'#FFFFFF',
|
|
'#A9A9A9',
|
|
] as const;
|
|
|
|
const FIELD_LABELS: Record<string, string> = {
|
|
destCityId: '대상 도시',
|
|
destCityID: '대상 도시',
|
|
destGeneralId: '대상 장수',
|
|
destGeneralID: '대상 장수',
|
|
destNationId: '대상 국가',
|
|
nationName: '국가명',
|
|
nationType: '국가 성향',
|
|
colorType: '국기 색상',
|
|
srcArmType: '기존 병과',
|
|
destArmType: '변경 병과',
|
|
itemType: '장비 종류',
|
|
itemCode: '장비',
|
|
crewType: '병종',
|
|
amount: '수량',
|
|
buyRice: '거래',
|
|
isGold: '물자',
|
|
optionText: '행동',
|
|
year: '기간(년)',
|
|
month: '기간(월)',
|
|
commandType: '대응 명령',
|
|
amountList: '지원 물자',
|
|
};
|
|
|
|
const OPTION_SOURCES: Record<string, TurnCommandOptionSource> = {
|
|
destCityId: 'cities',
|
|
destCityID: 'cities',
|
|
destGeneralId: 'generals',
|
|
destGeneralID: 'generals',
|
|
destNationId: 'nations',
|
|
nationType: 'nationTypes',
|
|
colorType: 'colors',
|
|
srcArmType: 'armTypes',
|
|
destArmType: 'armTypes',
|
|
itemCode: 'items',
|
|
crewType: 'crewTypes',
|
|
};
|
|
|
|
const STATIC_LABELS: Record<string, Record<string, string>> = {
|
|
itemType: {
|
|
horse: '명마',
|
|
weapon: '무기',
|
|
book: '서적',
|
|
item: '도구',
|
|
},
|
|
commandType: {
|
|
che_선전포고: '선전포고',
|
|
che_불가침제의: '불가침 제의',
|
|
che_불가침파기제의: '불가침 파기 제의',
|
|
che_종전제의: '종전 제의',
|
|
},
|
|
};
|
|
|
|
type JsonSchema = Record<string, unknown>;
|
|
|
|
const collectObjectSchema = (schema: JsonSchema): JsonSchema => {
|
|
const branches = schema.oneOf ?? schema.anyOf;
|
|
if (Array.isArray(branches)) {
|
|
const objectBranches = branches.filter(isRecord).map((branch) => collectObjectSchema(branch));
|
|
const properties = Object.assign({}, ...objectBranches.map((branch) => asRecord(branch.properties)));
|
|
const required = Array.from(
|
|
new Set(
|
|
objectBranches.flatMap((branch) =>
|
|
Array.isArray(branch.required)
|
|
? branch.required.filter((entry): entry is string => typeof entry === 'string')
|
|
: []
|
|
)
|
|
)
|
|
);
|
|
return { ...schema, properties, required };
|
|
}
|
|
return schema;
|
|
};
|
|
|
|
const enumOptions = (fieldKey: string, values: unknown[]): TurnCommandOption[] =>
|
|
values
|
|
.filter((value): value is TurnCommandOptionValue => typeof value === 'string' || typeof value === 'number')
|
|
.filter((value) => fieldKey !== 'commandType' || value !== 'che_피장파장')
|
|
.map((value) => ({
|
|
value,
|
|
label: STATIC_LABELS[fieldKey]?.[String(value)] ?? String(value),
|
|
}));
|
|
|
|
const buildField = (key: string, rawSchema: unknown, required: boolean): TurnCommandInputField => {
|
|
const schema = asRecord(rawSchema);
|
|
const label = FIELD_LABELS[key] ?? key;
|
|
const constValue = schema.const;
|
|
if (typeof constValue === 'string' || typeof constValue === 'number') {
|
|
return { key, label, kind: 'hidden', required, constValue };
|
|
}
|
|
|
|
if (key === 'amountList') {
|
|
return {
|
|
key,
|
|
label,
|
|
kind: 'numberTuple',
|
|
required,
|
|
tupleLabels: ['금', '쌀'],
|
|
min: 0,
|
|
step: 1,
|
|
};
|
|
}
|
|
|
|
const optionSource = OPTION_SOURCES[key];
|
|
const enumValues = Array.isArray(schema.enum) ? enumOptions(key, schema.enum) : undefined;
|
|
if (optionSource || enumValues) {
|
|
return {
|
|
key,
|
|
label,
|
|
kind: 'select',
|
|
required,
|
|
optionSource,
|
|
options: enumValues,
|
|
};
|
|
}
|
|
|
|
if (schema.type === 'boolean') {
|
|
return { key, label, kind: 'boolean', required };
|
|
}
|
|
if (schema.type === 'number' || schema.type === 'integer') {
|
|
return {
|
|
key,
|
|
label,
|
|
kind: 'number',
|
|
required,
|
|
min: typeof schema.minimum === 'number' ? schema.minimum : undefined,
|
|
max: typeof schema.maximum === 'number' ? schema.maximum : undefined,
|
|
step: schema.type === 'integer' ? 1 : undefined,
|
|
};
|
|
}
|
|
if (schema.type === 'string') {
|
|
return {
|
|
key,
|
|
label,
|
|
kind: 'text',
|
|
required,
|
|
min: typeof schema.minLength === 'number' ? schema.minLength : undefined,
|
|
max: typeof schema.maxLength === 'number' ? schema.maxLength : undefined,
|
|
};
|
|
}
|
|
|
|
throw new Error(`Unsupported turn command argument schema: ${key}`);
|
|
};
|
|
|
|
export const buildTurnCommandInputFields = (
|
|
spec: GeneralTurnCommandSpec | NationTurnCommandSpec
|
|
): TurnCommandInputField[] => {
|
|
if (!spec.reqArg) {
|
|
return [];
|
|
}
|
|
const jsonSchema = collectObjectSchema(asRecord(z.toJSONSchema(spec.argsSchema)));
|
|
const properties = asRecord(jsonSchema.properties);
|
|
const required = new Set(
|
|
Array.isArray(jsonSchema.required)
|
|
? jsonSchema.required.filter((entry): entry is string => typeof entry === 'string')
|
|
: []
|
|
);
|
|
return Object.entries(properties).map(([key, schema]) => buildField(key, schema, required.has(key)));
|
|
};
|
|
|
|
export const loadTurnCommandSpecs = async () => {
|
|
const profile = await loadTurnCommandProfile();
|
|
const [general, nation] = await Promise.all([
|
|
loadGeneralTurnCommandSpecs(profile.general),
|
|
loadNationTurnCommandSpecs(profile.nation),
|
|
]);
|
|
return { general, nation };
|
|
};
|
|
|
|
export const parseReservedTurnArgs = async (
|
|
scope: 'general' | 'nation',
|
|
action: string,
|
|
rawArgs: unknown
|
|
): Promise<Record<string, unknown>> => {
|
|
const specs = await loadTurnCommandSpecs();
|
|
const spec = specs[scope].find((entry) => entry.key === action);
|
|
if (!spec) {
|
|
throw new Error(`Unknown ${scope} turn command: ${action}`);
|
|
}
|
|
if (!spec.reqArg) {
|
|
return {};
|
|
}
|
|
return spec.argsSchema.parse(rawArgs);
|
|
};
|