커맨드의 arg 타입에 zod 강제.
This commit is contained in:
@@ -55,23 +55,22 @@ export interface ActionContextWorldRef {
|
||||
getTroopById(id: number): Troop | null;
|
||||
}
|
||||
|
||||
export interface ActionContextOptions {
|
||||
export interface ActionContextOptions<TArgs extends Record<string, unknown> = Record<string, unknown>> {
|
||||
world: ActionContextWorldState;
|
||||
scenarioConfig: ScenarioConfig;
|
||||
scenarioMeta?: ScenarioMeta;
|
||||
map?: MapDefinition;
|
||||
unitSet?: UnitSetDefinition;
|
||||
worldRef: ActionContextWorldRef | null;
|
||||
actionArgs: Record<string, unknown>;
|
||||
actionArgs: TArgs;
|
||||
createGeneralId: () => number;
|
||||
createNationId: () => number;
|
||||
seedBase: string;
|
||||
}
|
||||
|
||||
// 예약 턴 처리에서 커맨드별로 필요한 컨텍스트를 확장한다.
|
||||
export type ActionContextBuilder = (
|
||||
base: ActionContextBase,
|
||||
options: ActionContextOptions
|
||||
) => ActionResolveContext | null;
|
||||
export type ActionContextBuilder<TArgs extends Record<string, unknown> = Record<string, unknown>> = {
|
||||
bivarianceHack(base: ActionContextBase, options: ActionContextOptions<TArgs>): ActionResolveContext | null;
|
||||
}['bivarianceHack'];
|
||||
|
||||
export const defaultActionContextBuilder: ActionContextBuilder = (base) => base;
|
||||
|
||||
@@ -3,16 +3,35 @@ import type { ZodType } from 'zod';
|
||||
import type { ActionContextBuilder } from './actionContext.js';
|
||||
import type { TurnCommandEnv } from './commandEnv.js';
|
||||
|
||||
export interface TurnCommandSpecBase<TKey extends string = string> {
|
||||
export interface TurnCommandSpecWithArgs<TKey extends string = string> {
|
||||
key: TKey;
|
||||
category: string;
|
||||
reqArg: boolean;
|
||||
reqArg: true;
|
||||
args: Record<string, unknown>;
|
||||
argsSchema?: ZodType<unknown>;
|
||||
argsSchema: ZodType<Record<string, unknown>>;
|
||||
createDefinition(env: TurnCommandEnv): GeneralActionDefinition;
|
||||
}
|
||||
|
||||
export interface TurnCommandSpecWithoutArgs<TKey extends string = string> {
|
||||
key: TKey;
|
||||
category: string;
|
||||
reqArg: false;
|
||||
args: Record<string, unknown>;
|
||||
argsSchema?: undefined;
|
||||
createDefinition(env: TurnCommandEnv): GeneralActionDefinition;
|
||||
}
|
||||
|
||||
export type TurnCommandSpecBase<TKey extends string = string> =
|
||||
| TurnCommandSpecWithArgs<TKey>
|
||||
| TurnCommandSpecWithoutArgs<TKey>;
|
||||
|
||||
export type TurnCommandArgs<TSpec extends TurnCommandSpecBase> = TSpec extends {
|
||||
argsSchema: ZodType<infer TArgs>;
|
||||
}
|
||||
? TArgs
|
||||
: Record<string, never>;
|
||||
|
||||
export interface TurnCommandModule<TSpec extends TurnCommandSpecBase = TurnCommandSpecBase> {
|
||||
commandSpec: TSpec;
|
||||
actionContextBuilder?: ActionContextBuilder;
|
||||
actionContextBuilder?: ActionContextBuilder<TurnCommandArgs<TSpec>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { ActionDefinition as RecruitActionDefinition } from './che_징병.js';
|
||||
import { ARGS_SCHEMA, ActionDefinition as RecruitActionDefinition } from './che_징병.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -30,5 +30,6 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
crewType: 'number',
|
||||
amount: 'number',
|
||||
},
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ const DEFAULT_ATMOS = 40;
|
||||
const DEFAULT_MIN_POP = 30000;
|
||||
const DEFAULT_TRUST = 50;
|
||||
const MIN_CREW = 100;
|
||||
const ARGS_SCHEMA = z.preprocess(
|
||||
export const ARGS_SCHEMA = z.preprocess(
|
||||
(raw) => {
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
return raw;
|
||||
|
||||
@@ -12,10 +12,8 @@ import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface ChangeFlagArgs {
|
||||
colorType: number;
|
||||
}
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
const ACTION_NAME = '국기변경';
|
||||
|
||||
@@ -55,6 +53,11 @@ const NATION_COLORS = [
|
||||
'#A9A9A9',
|
||||
];
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
colorType: z.number().int().min(0).max(NATION_COLORS.length - 1),
|
||||
});
|
||||
export type ChangeFlagArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, ChangeFlagArgs> {
|
||||
@@ -62,10 +65,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): ChangeFlagArgs | null {
|
||||
const data = raw as { colorType?: number };
|
||||
if (typeof data?.colorType !== 'number') return null;
|
||||
if (data.colorType < 0 || data.colorType >= NATION_COLORS.length) return null;
|
||||
return { colorType: data.colorType };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: ChangeFlagArgs): Constraint[] {
|
||||
@@ -162,5 +162,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '국가',
|
||||
reqArg: true,
|
||||
args: { colorType: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -12,10 +12,13 @@ import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface ChangeNationNameArgs {
|
||||
nationName: string;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
nationName: z.string().trim().min(1).max(8),
|
||||
});
|
||||
export type ChangeNationNameArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
const ACTION_NAME = '국호변경';
|
||||
|
||||
@@ -26,13 +29,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): ChangeNationNameArgs | null {
|
||||
const data = raw as { nationName?: string };
|
||||
if (typeof data?.nationName !== 'string') return null;
|
||||
|
||||
const name = data.nationName.trim();
|
||||
if (name.length < 1 || name.length > 8) return null;
|
||||
|
||||
return { nationName: name };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: ChangeNationNameArgs): Constraint[] {
|
||||
@@ -124,5 +121,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '국가',
|
||||
reqArg: true,
|
||||
args: { nationName: '' },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -22,10 +22,16 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import { buildDefaultDiplomacy } from '../../../diplomacy/index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface RaidArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type RaidArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface RaidResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -43,14 +49,6 @@ const PRE_REQ_TURN = 0;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const TERM_REDUCE = 3;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
// 급습 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
@@ -173,12 +171,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): RaidArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: RaidArgs): Constraint[] {
|
||||
@@ -199,7 +192,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 국가/외교 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<RaidArgs> = (base, options) => {
|
||||
const destNationId = options.actionArgs.destNationId;
|
||||
if (typeof destNationId !== 'number') {
|
||||
return null;
|
||||
@@ -236,5 +229,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -20,12 +20,18 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { clamp } from 'es-toolkit';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface SeizureArgs {
|
||||
isGold: boolean;
|
||||
amount: number;
|
||||
destGeneralID: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
isGold: z.boolean(),
|
||||
amount: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value / 100) * 100 : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
destGeneralID: z.number(),
|
||||
});
|
||||
export type SeizureArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface SeizureResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -44,18 +50,13 @@ export class ActionDefinition<
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
parseArgs(raw: unknown): SeizureArgs | null {
|
||||
const data = raw as { isGold?: boolean; amount?: number; destGeneralID?: number };
|
||||
if (typeof data?.isGold !== 'boolean') return null;
|
||||
if (typeof data?.amount !== 'number') return null;
|
||||
if (typeof data?.destGeneralID !== 'number') return null;
|
||||
|
||||
const amount = Math.floor(data.amount / 100) * 100;
|
||||
if (amount <= 0) return null;
|
||||
|
||||
const data = parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
isGold: data.isGold,
|
||||
amount: clamp(amount, 100, this.env.maxResourceActionAmount ?? 10000),
|
||||
destGeneralID: data.destGeneralID,
|
||||
...data,
|
||||
amount: clamp(data.amount, 100, this.env.maxResourceActionAmount ?? 10000),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<SeizureArgs> = (base, options) => {
|
||||
const destGeneralId = options.actionArgs.destGeneralID;
|
||||
if (typeof destGeneralId !== 'number') return null;
|
||||
|
||||
@@ -161,5 +162,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '인사',
|
||||
reqArg: true,
|
||||
args: { isGold: false, amount: 0, destGeneralID: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||
};
|
||||
|
||||
@@ -20,11 +20,14 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { clamp } from 'es-toolkit';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface MaterialAidArgs {
|
||||
destNationId: number;
|
||||
amountList: [number, number]; // [gold, rice]
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.number(),
|
||||
amountList: z.tuple([z.number(), z.number()]),
|
||||
});
|
||||
export type MaterialAidArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface MaterialAidResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -45,15 +48,7 @@ export class ActionDefinition<
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
parseArgs(raw: unknown): MaterialAidArgs | null {
|
||||
const data = raw as { destNationId?: number; amountList?: [number, number] };
|
||||
if (typeof data?.destNationId !== 'number') return null;
|
||||
if (!Array.isArray(data.amountList) || data.amountList.length !== 2) return null;
|
||||
if (typeof data.amountList[0] !== 'number' || typeof data.amountList[1] !== 'number') return null;
|
||||
|
||||
return {
|
||||
destNationId: data.destNationId,
|
||||
amountList: [data.amountList[0], data.amountList[1]],
|
||||
};
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] {
|
||||
@@ -187,7 +182,7 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<MaterialAidArgs> = (base, options) => {
|
||||
const destNationId = options.actionArgs.destNationId;
|
||||
if (typeof destNationId !== 'number') return null;
|
||||
|
||||
@@ -208,5 +203,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0, amountList: [0, 0] },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||
};
|
||||
|
||||
@@ -25,11 +25,14 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import { resolveTurnTermMinutes } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface AssignmentArgs {
|
||||
destGeneralId: number;
|
||||
destCityId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destGeneralId: z.number(),
|
||||
destCityId: z.number(),
|
||||
});
|
||||
export type AssignmentArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface AssignmentResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -146,23 +149,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): AssignmentArgs | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const data = raw as {
|
||||
destGeneralId?: unknown;
|
||||
destCityId?: unknown;
|
||||
};
|
||||
if (typeof data.destGeneralId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
if (typeof data.destCityId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
destGeneralId: data.destGeneralId,
|
||||
destCityId: data.destCityId,
|
||||
};
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: AssignmentArgs): Constraint[] {
|
||||
@@ -188,7 +175,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 장수/도시 컨텍스트를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<AssignmentArgs> = (base, options) => {
|
||||
const destGeneralId = options.actionArgs.destGeneralId;
|
||||
const destCityId = options.actionArgs.destCityId;
|
||||
if (typeof destGeneralId !== 'number' || typeof destCityId !== 'number') {
|
||||
@@ -216,5 +203,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '인사',
|
||||
reqArg: true,
|
||||
args: { destGeneralId: 0, destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition({}),
|
||||
};
|
||||
|
||||
@@ -20,10 +20,16 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface MobilizePeopleArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type MobilizePeopleArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface MobilizePeopleResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -38,14 +44,6 @@ const PRE_REQ_TURN = 0;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const DEFENCE_RATE = 0.8;
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
// 백성동원 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
@@ -151,12 +149,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): MobilizePeopleArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: MobilizePeopleArgs): Constraint[] {
|
||||
@@ -174,7 +167,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시/장수 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<MobilizePeopleArgs> = (base, options) => {
|
||||
const destCityId = options.actionArgs.destCityId;
|
||||
if (typeof destCityId !== 'number') {
|
||||
return null;
|
||||
@@ -200,5 +193,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -19,10 +19,16 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface TroopKickArgs {
|
||||
destGeneralId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destGeneralId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type TroopKickArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface TroopKickResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -39,17 +45,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): TroopKickArgs | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const data = raw as { destGeneralId?: unknown };
|
||||
if (typeof data.destGeneralId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
if (data.destGeneralId <= 0) {
|
||||
return null;
|
||||
}
|
||||
return { destGeneralId: data.destGeneralId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: TroopKickArgs): Constraint[] {
|
||||
@@ -98,7 +94,7 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<TroopKickArgs> = (base, options) => {
|
||||
const destGeneralId = options.actionArgs.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return null;
|
||||
@@ -118,5 +114,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '인사',
|
||||
reqArg: true,
|
||||
args: { destGeneralId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -15,38 +15,25 @@ import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface NonAggressionProposalArgs {
|
||||
destNationId: number;
|
||||
year: number;
|
||||
month: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
year: z.preprocess((value) => (typeof value === 'number' ? Math.floor(value) : value), z.number().int().min(0)),
|
||||
month: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().min(1).max(12)
|
||||
),
|
||||
});
|
||||
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
const ACTION_NAME = '불가침 제의';
|
||||
const MIN_TERM_MONTHS = 6;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseYear = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw >= 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseMonth = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const month = Math.floor(raw);
|
||||
return month >= 1 && month <= 12 ? month : null;
|
||||
};
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const requireMinimumTerm = (minMonths: number): Constraint => ({
|
||||
@@ -107,18 +94,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): NonAggressionProposalArgs | null {
|
||||
const data = raw as {
|
||||
destNationId?: unknown;
|
||||
year?: unknown;
|
||||
month?: unknown;
|
||||
};
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
const year = parseYear(data?.year);
|
||||
const month = parseMonth(data?.month);
|
||||
if (destNationId === null || year === null || month === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId, year, month };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionProposalArgs): Constraint[] {
|
||||
@@ -163,5 +139,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0, year: 0, month: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -15,21 +15,20 @@ import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface NonAggressionCancelProposalArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
const ACTION_NAME = '불가침 파기 제의';
|
||||
const DIPLOMACY_NON_AGGRESSION = 7;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
// 불가침 파기 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -38,12 +37,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): NonAggressionCancelProposalArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelProposalArgs): Constraint[] {
|
||||
@@ -81,5 +75,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -21,10 +21,16 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface DeclareWarArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type DeclareWarArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
// DeclareWarResolveContext is not used anymore as it was replaced by inline type in ActionDefinition
|
||||
|
||||
@@ -33,13 +39,6 @@ const ACTION_NAME = '선전포고';
|
||||
const DIPLOMACY_DECLARE = 1;
|
||||
const DECLARE_TERM = 24;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<
|
||||
@@ -51,12 +50,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): DeclareWarArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DeclareWarArgs): Constraint[] {
|
||||
@@ -164,7 +158,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 국가 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<DeclareWarArgs> = (base, options) => {
|
||||
const destNationId = options.actionArgs.destNationId;
|
||||
if (typeof destNationId !== 'number') {
|
||||
return null;
|
||||
@@ -188,5 +182,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -22,10 +22,16 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface FloodArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type FloodArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface FloodResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -42,14 +48,6 @@ const PRE_REQ_TURN = 2;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const DAMAGE_RATE = 0.2;
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
// 수몰 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
@@ -176,12 +174,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): FloodArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: FloodArgs): Constraint[] {
|
||||
@@ -203,7 +196,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시/장수 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<FloodArgs> = (base, options) => {
|
||||
const destCityId = options.actionArgs.destCityId;
|
||||
if (typeof destCityId !== 'number') {
|
||||
return null;
|
||||
@@ -234,5 +227,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -22,10 +22,16 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import { buildDefaultDiplomacy, DIPLOMACY_STATE } from '../../../diplomacy/index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface DegradeRelationsArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type DegradeRelationsArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface DegradeRelationsResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -42,14 +48,6 @@ const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 0;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const resolveNextTerm = (state: number, term: number): number => (state === DIPLOMACY_STATE.WAR ? 3 : term + 3);
|
||||
|
||||
// 이호경식 쿨타임 계산을 담당한다.
|
||||
@@ -180,12 +178,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DegradeRelationsArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DegradeRelationsArgs): Constraint[] {
|
||||
@@ -209,7 +202,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 국가/외교 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<DegradeRelationsArgs> = (base, options) => {
|
||||
const destNationId = options.actionArgs.destNationId;
|
||||
if (typeof destNationId !== 'number') {
|
||||
return null;
|
||||
@@ -246,5 +239,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -16,20 +16,19 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface StopWarProposalArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
const ACTION_NAME = '종전 제의';
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
// 종전 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -38,12 +37,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): StopWarProposalArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: StopWarProposalArgs): Constraint[] {
|
||||
@@ -84,5 +78,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -20,10 +20,13 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface MoveCapitalArgs {
|
||||
destCityID: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityID: z.number(),
|
||||
});
|
||||
export type MoveCapitalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface MoveCapitalResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -71,9 +74,7 @@ export class ActionDefinition<
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
parseArgs(raw: unknown): MoveCapitalArgs | null {
|
||||
const data = raw as { destCityID?: number };
|
||||
if (typeof data?.destCityID !== 'number') return null;
|
||||
return { destCityID: data.destCityID };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: MoveCapitalArgs): Constraint[] {
|
||||
@@ -194,7 +195,7 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<MoveCapitalArgs> = (base, options) => {
|
||||
const destCityId = options.actionArgs.destCityID;
|
||||
if (typeof destCityId !== 'number') return null;
|
||||
|
||||
@@ -217,5 +218,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '국가',
|
||||
reqArg: true,
|
||||
args: { destCityID: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||
};
|
||||
|
||||
@@ -21,10 +21,16 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface ScorchedEarthArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type ScorchedEarthArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface ScorchedEarthResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -37,14 +43,6 @@ const ACTION_NAME = '초토화';
|
||||
const PRE_REQ_TURN = 2;
|
||||
const POST_REQ_TURN = 24;
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const requireNoDiplomacyLimit = (): Constraint => ({
|
||||
name: 'requireNoDiplomacyLimit',
|
||||
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
|
||||
@@ -115,12 +113,7 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): ScorchedEarthArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: ScorchedEarthArgs): Constraint[] {
|
||||
@@ -247,7 +240,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<ScorchedEarthArgs> = (base, options) => {
|
||||
const destCityId = options.actionArgs.destCityId;
|
||||
if (typeof destCityId !== 'number') {
|
||||
return null;
|
||||
@@ -273,5 +266,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '특수',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -25,12 +25,15 @@ import type { NationTurnCommandSpec } from './index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { clamp } from 'es-toolkit';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface AwardArgs {
|
||||
isGold: boolean;
|
||||
amount: number;
|
||||
destGeneralId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
isGold: z.boolean(),
|
||||
amount: z.number(),
|
||||
destGeneralId: z.number(),
|
||||
});
|
||||
export type AwardArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface AwardResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -163,21 +166,8 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): AwardArgs | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const data = raw as {
|
||||
isGold?: unknown;
|
||||
amount?: unknown;
|
||||
destGeneralId?: unknown;
|
||||
};
|
||||
if (typeof data.isGold !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
if (typeof data.amount !== 'number' || Number.isNaN(data.amount)) {
|
||||
return null;
|
||||
}
|
||||
if (typeof data.destGeneralId !== 'number') {
|
||||
const data = parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const amount = this.command.normalizeAmount(data.amount);
|
||||
@@ -185,9 +175,8 @@ export class ActionDefinition<
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
isGold: data.isGold,
|
||||
...data,
|
||||
amount,
|
||||
destGeneralId: data.destGeneralId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,7 +220,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 장수를 주입한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<AwardArgs> = (base, options) => {
|
||||
const destGeneralId = options.actionArgs.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return null;
|
||||
@@ -251,6 +240,7 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '인사',
|
||||
reqArg: true,
|
||||
args: { isGold: true, amount: 1, destGeneralId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => {
|
||||
const maxAmount =
|
||||
env.maxResourceActionAmount > 0 ? env.maxResourceActionAmount : Math.max(env.baseGold, env.baseRice, 1000);
|
||||
|
||||
@@ -21,11 +21,10 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface CounterStrategyArgs {
|
||||
destNationId: number;
|
||||
commandType: string;
|
||||
}
|
||||
export type CounterStrategyArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface CounterStrategyResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -40,7 +39,18 @@ const DEFAULT_GLOBAL_DELAY = 8;
|
||||
const PRE_REQ_TURN = 1;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
|
||||
const STRATEGIC_COMMANDS: Record<string, string> = {
|
||||
const STRATEGIC_COMMAND_KEYS = [
|
||||
'che_필사즉생',
|
||||
'che_백성동원',
|
||||
'che_수몰',
|
||||
'che_허보',
|
||||
'che_의병모집',
|
||||
'che_이호경식',
|
||||
'che_급습',
|
||||
'che_피장파장',
|
||||
] as const;
|
||||
|
||||
const STRATEGIC_COMMANDS: Record<(typeof STRATEGIC_COMMAND_KEYS)[number], string> = {
|
||||
che_필사즉생: '필사즉생',
|
||||
che_백성동원: '백성동원',
|
||||
che_수몰: '수몰',
|
||||
@@ -51,26 +61,15 @@ const STRATEGIC_COMMANDS: Record<string, string> = {
|
||||
che_피장파장: '피장파장',
|
||||
};
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const parseCommandType = (raw: unknown): string | null => {
|
||||
if (typeof raw !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(STRATEGIC_COMMANDS, raw)) {
|
||||
return null;
|
||||
}
|
||||
if (raw === 'che_피장파장') {
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
};
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
commandType: z
|
||||
.enum(STRATEGIC_COMMAND_KEYS)
|
||||
.refine((value) => value !== 'che_피장파장', '같은 전략은 선택할 수 없습니다.'),
|
||||
});
|
||||
|
||||
const requireCommandType = (): Constraint => ({
|
||||
name: 'requireStrategicCommandType',
|
||||
@@ -209,13 +208,7 @@ export class ActionDefinition<
|
||||
private readonly resolver = new ActionResolver<TriggerState>();
|
||||
|
||||
parseArgs(raw: unknown): CounterStrategyArgs | null {
|
||||
const data = raw as { destNationId?: unknown; commandType?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
const commandType = parseCommandType(data?.commandType);
|
||||
if (destNationId === null || commandType === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId, commandType };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: CounterStrategyArgs): Constraint[] {
|
||||
@@ -237,7 +230,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 국가/장수 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<CounterStrategyArgs> = (base, options) => {
|
||||
const destNationId = options.actionArgs.destNationId;
|
||||
if (typeof destNationId !== 'number') {
|
||||
return null;
|
||||
@@ -266,5 +259,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0, commandType: '' },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -22,10 +22,16 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface DeceptionArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
});
|
||||
export type DeceptionArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface DeceptionResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -42,14 +48,6 @@ const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 1;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: number, candidates: City[]): number => {
|
||||
if (candidates.length === 0) {
|
||||
return destCityId;
|
||||
@@ -183,12 +181,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DeceptionArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DeceptionArgs): Constraint[] {
|
||||
@@ -210,7 +203,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시/장수 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<DeceptionArgs> = (base, options) => {
|
||||
const destCityId = options.actionArgs.destCityId;
|
||||
if (typeof destCityId !== 'number') {
|
||||
return null;
|
||||
@@ -247,5 +240,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -25,11 +25,8 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { clamp } from 'es-toolkit';
|
||||
|
||||
export interface PopulationMoveArgs {
|
||||
destCityId: number;
|
||||
amount: number;
|
||||
}
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
export interface PopulationMoveResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -42,24 +39,17 @@ const ACTION_NAME = '인구이동';
|
||||
const AMOUNT_LIMIT = 100000;
|
||||
const MIN_AVAILABLE_RECRUIT_POP = 30000;
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const parseAmount = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
if (value < 0) {
|
||||
return null;
|
||||
}
|
||||
return clamp(value, 0, AMOUNT_LIMIT);
|
||||
};
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
amount: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? clamp(Math.floor(value), 0, AMOUNT_LIMIT) : value),
|
||||
z.number().int().min(0).max(AMOUNT_LIMIT)
|
||||
),
|
||||
});
|
||||
export type PopulationMoveArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
const calcCost = (develCost: number, amount: number): number => Math.round((develCost * amount) / 10000);
|
||||
|
||||
@@ -72,13 +62,7 @@ export class ActionDefinition<
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
parseArgs(raw: unknown): PopulationMoveArgs | null {
|
||||
const data = raw as { destCityId?: unknown; amount?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
const amount = parseAmount(data?.amount);
|
||||
if (destCityId === null || amount === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId, amount };
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: PopulationMoveArgs): Constraint[] {
|
||||
@@ -159,7 +143,7 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
export const actionContextBuilder: ActionContextBuilder<PopulationMoveArgs> = (base, options) => {
|
||||
const destCityId = options.actionArgs.destCityId;
|
||||
if (typeof destCityId !== 'number') {
|
||||
return null;
|
||||
@@ -185,5 +169,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '특수',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0, amount: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||
};
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('Nation Missing Actions', () => {
|
||||
setupDiplomacy(view, nation.id, destNation.id, 2);
|
||||
|
||||
const definition = new CounterStrategyAction();
|
||||
const args = { destNationId: destNation.id, commandType: 'che_허보' };
|
||||
const args = { destNationId: destNation.id, commandType: 'che_허보' } as const;
|
||||
const ctx: ConstraintContext = {
|
||||
actorId: general.id,
|
||||
nationId: nation.id,
|
||||
@@ -327,7 +327,7 @@ describe('Nation Missing Actions', () => {
|
||||
addLog: () => {},
|
||||
} as any,
|
||||
{ now: new Date(), schedule },
|
||||
{ destNationId: destNation.id, commandType: 'che_허보' }
|
||||
{ destNationId: destNation.id, commandType: 'che_허보' } as const
|
||||
);
|
||||
|
||||
expect(resolution.general.experience).toBeGreaterThan(100);
|
||||
|
||||
Reference in New Issue
Block a user