Merge feature/nation-turn-audit
This commit is contained in:
@@ -14,5 +14,10 @@ export interface GeneralActionDefinition<
|
||||
// 커맨드 입력 단계에서 최소 조건만 평가할 때 사용한다.
|
||||
buildMinConstraints?(ctx: ConstraintContext, args: Args): Constraint[];
|
||||
buildConstraints(ctx: ConstraintContext, args: Args): Constraint[];
|
||||
// NationCommand::addTermStack()/setNextAvailable() 호환 실행 메타데이터.
|
||||
getPreReqTurn?(context: Context, args: Args): number;
|
||||
getPostReqTurn?(context: Context, args: Args): number;
|
||||
getStackSequence?(context: Context, args: Args): number | null;
|
||||
readonly countsAsInheritanceActiveAction?: boolean;
|
||||
resolve(context: Context, args: Args): GeneralActionOutcome<TriggerState>;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import { getNextTurnAt, type TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
|
||||
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { MessageDraft } from '@sammo-ts/logic/messages/message.js';
|
||||
|
||||
enablePatches();
|
||||
|
||||
@@ -86,6 +87,11 @@ export interface NextTurnOverrideEffect {
|
||||
nextTurnAt: Date;
|
||||
}
|
||||
|
||||
export interface MessageAddEffect {
|
||||
type: 'message:add';
|
||||
draft: MessageDraft;
|
||||
}
|
||||
|
||||
export type GeneralActionEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
|
||||
| GeneralPatchEffect<TriggerState>
|
||||
| GeneralAddEffect<TriggerState>
|
||||
@@ -94,6 +100,7 @@ export type GeneralActionEffect<TriggerState extends GeneralTriggerState = Gener
|
||||
| NationAddEffect
|
||||
| DiplomacyPatchEffect
|
||||
| LogEffect
|
||||
| MessageAddEffect
|
||||
| NextTurnOverrideEffect;
|
||||
|
||||
export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
@@ -203,6 +210,11 @@ export const createLogEffect = (message: string, options: Partial<Omit<LogEntryD
|
||||
},
|
||||
});
|
||||
|
||||
export const createMessageEffect = (draft: MessageDraft): MessageAddEffect => ({
|
||||
type: 'message:add',
|
||||
draft,
|
||||
});
|
||||
|
||||
export const createNextTurnOverrideEffect = (nextTurnAt: Date): NextTurnOverrideEffect => ({
|
||||
type: 'schedule:override',
|
||||
nextTurnAt,
|
||||
@@ -301,6 +313,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
|
||||
createdNations.push(effect.nation as Nation);
|
||||
break;
|
||||
case 'diplomacy:patch':
|
||||
case 'message:add':
|
||||
pendingEffects.push(effect);
|
||||
break;
|
||||
case 'general:patch':
|
||||
|
||||
@@ -47,14 +47,15 @@ const requireCapitalCity = (reason: string): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
const reqDestCityValue = (
|
||||
comp: '>' | '<' | '>=' | '<=',
|
||||
required: number | 'origin',
|
||||
reason: string
|
||||
): Constraint => ({
|
||||
const reqDestCityValue = (comp: '>' | '<' | '>=' | '<=', required: number | 'origin', reason: string): Constraint => ({
|
||||
name: 'reqDestCityValue',
|
||||
requires: (ctx) =>
|
||||
ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }, { kind: 'env', key: 'map' }] : [],
|
||||
ctx.nationId !== undefined
|
||||
? [
|
||||
{ kind: 'nation', id: ctx.nationId },
|
||||
{ kind: 'env', key: 'map' },
|
||||
]
|
||||
: [],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
if (ctx.nationId === undefined) {
|
||||
return { kind: 'deny', reason };
|
||||
@@ -71,8 +72,7 @@ const reqDestCityValue = (
|
||||
required === 'origin'
|
||||
? ((view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined)?.cities.find(
|
||||
(mapCity) => mapCity.id === nation.capitalCityId
|
||||
)?.level ??
|
||||
0)
|
||||
)?.level ?? 0)
|
||||
: required;
|
||||
const level = city.level;
|
||||
const allow =
|
||||
@@ -95,6 +95,7 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, ReduceCityArgs, ReduceCityResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_감축';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
@@ -122,6 +123,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getStackSequence(context: ReduceCityResolveContext<TriggerState>): number {
|
||||
const value = context.nation?.meta.capset;
|
||||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: ReduceCityResolveContext<TriggerState>,
|
||||
_args: ReduceCityArgs
|
||||
@@ -163,6 +173,10 @@ export class ActionDefinition<
|
||||
{
|
||||
gold: nation.gold + recoverAmount,
|
||||
rice: nation.rice + recoverAmount,
|
||||
meta: {
|
||||
...nation.meta,
|
||||
capset: (typeof nation.meta.capset === 'number' ? nation.meta.capset : 0) + 1,
|
||||
},
|
||||
},
|
||||
nation.id
|
||||
),
|
||||
@@ -194,6 +208,11 @@ export class ActionDefinition<
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// General Action Log
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
|
||||
@@ -54,7 +54,11 @@ const NATION_COLORS = [
|
||||
];
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
colorType: z.number().int().min(0).max(NATION_COLORS.length - 1),
|
||||
colorType: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(NATION_COLORS.length - 1),
|
||||
});
|
||||
export type ChangeFlagArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
@@ -63,6 +67,7 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, ChangeFlagArgs> {
|
||||
public readonly key = 'che_국기변경';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
parseArgs(raw: unknown): ChangeFlagArgs | null {
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
@@ -141,6 +146,11 @@ export class ActionDefinition<
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`<span style='color:${color};'><b>국기</b></span>를 변경`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// General Action Log
|
||||
createLogEffect(`<span style='color:${color};'><b>국기</b></span>를 변경하였습니다`, {
|
||||
scope: LogScope.GENERAL,
|
||||
|
||||
@@ -27,6 +27,7 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, ChangeNationNameArgs> {
|
||||
public readonly key = 'che_국호변경';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
parseArgs(raw: unknown): ChangeNationNameArgs | null {
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
@@ -101,6 +102,11 @@ export class ActionDefinition<
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
createLogEffect(`국호를 <D><b>${newNationName}</b></>${josaRo} 변경`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// General Action Log
|
||||
createLogEffect(`국호를 <D><b>${newNationName}</b></>${josaRo} 변경합니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
|
||||
@@ -53,10 +53,19 @@ const TERM_REDUCE = 3;
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: RaidResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 16) * 10);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(context: RaidResolveContext<TriggerState>): number {
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
@@ -69,8 +78,12 @@ export class ActionResolver<
|
||||
readonly key = 'che_급습';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: RaidResolveContext<TriggerState>): number {
|
||||
return this.command.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(context: RaidResolveContext<TriggerState>, _args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
||||
@@ -167,8 +180,8 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): RaidArgs | null {
|
||||
@@ -191,6 +204,10 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPostReqTurn(context: RaidResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(context: RaidResolveContext<TriggerState>, args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
@@ -235,5 +252,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
reqArg: true,
|
||||
availabilityArgs: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { GeneralTriggerState, City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beLord, occupiedCity, suppliedCity, beOpeningPart, reqNationAuxValue } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
beLord,
|
||||
occupiedCity,
|
||||
suppliedCity,
|
||||
beOpeningPart,
|
||||
reqNationAuxValue,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
@@ -39,6 +45,7 @@ export class ActionDefinition<
|
||||
> {
|
||||
public readonly key = 'che_무작위수도이전';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
parseArgs(_raw: unknown): RandomMoveCapitalArgs | null {
|
||||
return {};
|
||||
@@ -57,6 +64,10 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RandomMoveCapitalResolveContext<TriggerState>,
|
||||
_args: RandomMoveCapitalArgs
|
||||
@@ -142,6 +153,11 @@ export class ActionDefinition<
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// General Action Log
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} 국가를 옮겼습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
beChief,
|
||||
@@ -27,16 +27,23 @@ import { clamp } from 'es-toolkit';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.number(),
|
||||
amountList: z.tuple([z.number(), z.number()]),
|
||||
});
|
||||
const ARGS_SCHEMA = z
|
||||
.object({
|
||||
destNationId: z.number().int().positive(),
|
||||
amountList: z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]),
|
||||
})
|
||||
.refine(({ amountList }) => amountList[0] > 0 || amountList[1] > 0, {
|
||||
message: '지원량은 0보다 커야 합니다.',
|
||||
path: ['amountList'],
|
||||
});
|
||||
export type MaterialAidArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
export interface MaterialAidResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
friendlyChiefs: Array<General<TriggerState>>;
|
||||
destNationChiefs: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '원조';
|
||||
@@ -75,7 +82,12 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildMinConstraints(_ctx: ConstraintContext, _args: MaterialAidArgs): Constraint[] {
|
||||
return [occupiedCity(), beChief(), suppliedCity(), reqNationValue('surlimit', '외교제한', '==', 0, '외교제한중입니다.')];
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
reqNationValue('surlimit', '외교제한', '==', 0, '외교제한중입니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] {
|
||||
@@ -111,11 +123,24 @@ export class ActionDefinition<
|
||||
|
||||
const goldText = actualGold.toLocaleString();
|
||||
const riceText = actualRice.toLocaleString();
|
||||
const nationName = nation.name;
|
||||
const josaUlRice = JosaUtil.pick(riceText, '을');
|
||||
const josaRo = JosaUtil.pick(destNation.name, '로');
|
||||
const nationName = nation.name;
|
||||
const josaRoSrc = JosaUtil.pick(nationName, '로');
|
||||
|
||||
const broadcastMessage = `<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>을 지원했습니다.`;
|
||||
const recvAssist =
|
||||
typeof destNation.meta.recv_assist === 'object' && destNation.meta.recv_assist !== null
|
||||
? { ...destNation.meta.recv_assist }
|
||||
: {};
|
||||
const recvKey = `n${nation.id}`;
|
||||
const priorEntry =
|
||||
typeof recvAssist[recvKey] === 'object' && recvAssist[recvKey] !== null ? recvAssist[recvKey] : {};
|
||||
const priorAmount = Number(priorEntry['1'] ?? 0);
|
||||
recvAssist[recvKey] = {
|
||||
0: nation.id,
|
||||
1: (Number.isFinite(priorAmount) ? priorAmount : 0) + actualGold + actualRice,
|
||||
};
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createNationPatchEffect(
|
||||
@@ -133,6 +158,10 @@ export class ActionDefinition<
|
||||
{
|
||||
gold: destNation.gold + actualGold,
|
||||
rice: destNation.rice + actualRice,
|
||||
meta: {
|
||||
...destNation.meta,
|
||||
recv_assist: recvAssist,
|
||||
},
|
||||
},
|
||||
destNation.id
|
||||
),
|
||||
@@ -146,6 +175,14 @@ export class ActionDefinition<
|
||||
}
|
||||
),
|
||||
// Actor Nation History Log
|
||||
createLogEffect(
|
||||
`<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(
|
||||
`<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원`,
|
||||
{
|
||||
@@ -157,7 +194,7 @@ export class ActionDefinition<
|
||||
),
|
||||
// Dest Nation History Log
|
||||
createLogEffect(
|
||||
`<D><b>${nationName}</b></>${JosaUtil.pick(nationName, '부터')} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원 받음`,
|
||||
`<D><b>${nationName}</b></>${josaRoSrc}부터 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원 받음`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
nationId: destNation.id,
|
||||
@@ -178,6 +215,30 @@ export class ActionDefinition<
|
||||
}),
|
||||
];
|
||||
|
||||
for (const chief of context.friendlyChiefs) {
|
||||
if (chief.id !== general.id) {
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: chief.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const destBroadcastMessage = `<D><b>${nationName}</b></>에서 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 원조했습니다.`;
|
||||
for (const chief of context.destNationChiefs) {
|
||||
effects.push(
|
||||
createLogEffect(destBroadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: chief.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
general.experience += 5;
|
||||
general.dedication += 5;
|
||||
|
||||
@@ -194,10 +255,15 @@ export const actionContextBuilder: ActionContextBuilder<MaterialAidArgs> = (base
|
||||
|
||||
const destNation = worldRef.getNationById(destNationId);
|
||||
if (!destNation) return null;
|
||||
const generals = worldRef.listGenerals();
|
||||
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
friendlyChiefs: generals.filter(
|
||||
(general) => general.nationId === base.general.nationId && general.officerLevel >= 5
|
||||
),
|
||||
destNationChiefs: generals.filter((general) => general.nationId === destNationId && general.officerLevel >= 5),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -48,10 +48,19 @@ const DEFENCE_RATE = 0.8;
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 4) * 10);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
@@ -64,8 +73,12 @@ export class ActionResolver<
|
||||
readonly key = 'che_백성동원';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||
return this.command.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -144,8 +157,8 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): MobilizePeopleArgs | null {
|
||||
@@ -162,6 +175,10 @@ export class ActionDefinition<
|
||||
return [occupiedCity(), beChief(), occupiedDestCity(), availableStrategicCommand()];
|
||||
}
|
||||
|
||||
getPostReqTurn(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: MobilizePeopleResolveContext<TriggerState>,
|
||||
args: MobilizePeopleArgs
|
||||
@@ -198,5 +215,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
reqArg: true,
|
||||
availabilityArgs: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
beChief,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
@@ -32,6 +32,14 @@ const ARGS_SCHEMA = z.object({
|
||||
});
|
||||
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
interface NonAggressionProposalContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
messageValidMinutes: number;
|
||||
messageTime: Date;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '불가침 제의';
|
||||
const MIN_TERM_MONTHS = 6;
|
||||
|
||||
@@ -90,7 +98,11 @@ const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
||||
// 불가침 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionProposalArgs> {
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
NonAggressionProposalArgs,
|
||||
NonAggressionProposalContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_불가침제의';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -117,14 +129,42 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
resolve(
|
||||
_context: GeneralActionResolveContext<TriggerState>,
|
||||
context: NonAggressionProposalContext<TriggerState>,
|
||||
args: NonAggressionProposalArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const destNationName =
|
||||
(_context as { destNation?: { name?: string } }).destNation?.name ?? `국가${args.destNationId}`;
|
||||
const { general, nation, destNation } = context;
|
||||
if (!nation) {
|
||||
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||
}
|
||||
const destNationName = destNation.name;
|
||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||
const josaWa = JosaUtil.pick(nation.name, '와');
|
||||
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||
return {
|
||||
effects: [
|
||||
createMessageEffect({
|
||||
msgType: 'diplomacy',
|
||||
src: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: nation.id,
|
||||
nationName: nation.name,
|
||||
color: nation.color,
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: 0,
|
||||
generalName: '',
|
||||
nationId: destNation.id,
|
||||
nationName: destNation.name,
|
||||
color: destNation.color,
|
||||
icon: '',
|
||||
},
|
||||
text: `${nation.name}${josaWa} ${args.year}년 ${args.month}월까지 불가침 제의 서신`,
|
||||
time: context.messageTime,
|
||||
validUntil,
|
||||
option: { action: 'noAggression', year: args.year, month: args.month },
|
||||
}),
|
||||
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 불가침 제의 서신을 보냈습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
@@ -136,11 +176,18 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 날짜 정보를 제공한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
|
||||
...base,
|
||||
currentYear: options.world.currentYear,
|
||||
currentMonth: options.world.currentMonth,
|
||||
});
|
||||
export const actionContextBuilder: ActionContextBuilder<NonAggressionProposalArgs> = (base, options) => {
|
||||
const destNation = options.worldRef?.getNationById(options.actionArgs.destNationId);
|
||||
if (!destNation) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
messageTime: base.general.turnTime,
|
||||
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_불가침제의',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
@@ -27,13 +27,25 @@ const ARGS_SCHEMA = z.object({
|
||||
});
|
||||
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
interface NonAggressionCancelProposalContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
messageValidMinutes: number;
|
||||
messageTime: Date;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '불가침 파기 제의';
|
||||
const DIPLOMACY_NON_AGGRESSION = 7;
|
||||
|
||||
// 불가침 파기 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs> {
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
NonAggressionCancelProposalArgs,
|
||||
NonAggressionCancelProposalContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_불가침파기제의';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -57,14 +69,41 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
resolve(
|
||||
_context: GeneralActionResolveContext<TriggerState>,
|
||||
args: NonAggressionCancelProposalArgs
|
||||
context: NonAggressionCancelProposalContext<TriggerState>,
|
||||
_args: NonAggressionCancelProposalArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const destNationName =
|
||||
(_context as { destNation?: { name?: string } }).destNation?.name ?? `국가${args.destNationId}`;
|
||||
const { general, nation, destNation } = context;
|
||||
if (!nation) {
|
||||
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||
}
|
||||
const destNationName = destNation.name;
|
||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||
return {
|
||||
effects: [
|
||||
createMessageEffect({
|
||||
msgType: 'diplomacy',
|
||||
src: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: nation.id,
|
||||
nationName: nation.name,
|
||||
color: nation.color,
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: 0,
|
||||
generalName: '',
|
||||
nationId: destNation.id,
|
||||
nationName: destNation.name,
|
||||
color: destNation.color,
|
||||
icon: '',
|
||||
},
|
||||
text: `${nation.name}의 불가침 파기 제의 서신`,
|
||||
time: context.messageTime,
|
||||
validUntil,
|
||||
option: { action: 'cancelNA', deletable: false },
|
||||
}),
|
||||
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 불가침 파기 제의 서신을 보냈습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
@@ -76,7 +115,18 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
||||
export const actionContextBuilder = defaultActionContextBuilder;
|
||||
export const actionContextBuilder: ActionContextBuilder<NonAggressionCancelProposalArgs> = (base, options) => {
|
||||
const destNation = options.worldRef?.getNationById(options.actionArgs.destNationId);
|
||||
if (!destNation) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
messageTime: base.general.turnTime,
|
||||
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_불가침파기제의',
|
||||
|
||||
@@ -58,10 +58,19 @@ const battleGroundCity = (): Constraint => ({
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: FloodResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 4) * 10);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(context: FloodResolveContext<TriggerState>): number {
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
@@ -74,8 +83,12 @@ export class ActionResolver<
|
||||
readonly key = 'che_수몰';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: FloodResolveContext<TriggerState>): number {
|
||||
return this.command.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(context: FloodResolveContext<TriggerState>, _args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
||||
@@ -175,8 +188,8 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): FloodArgs | null {
|
||||
@@ -200,6 +213,14 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return PRE_REQ_TURN;
|
||||
}
|
||||
|
||||
getPostReqTurn(context: FloodResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(context: FloodResolveContext<TriggerState>, args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
@@ -238,5 +259,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
reqArg: true,
|
||||
availabilityArgs: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -221,6 +221,11 @@ export class ActionResolver<
|
||||
this.command = new CommandResolver(modules, env);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: VolunteerRecruitResolveContext<TriggerState>): number {
|
||||
const value = context.nation ? readMetaNumber(context.nation.meta, 'gennum') : null;
|
||||
return this.command.getPostDelay(context, value ?? 0);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: VolunteerRecruitResolveContext<TriggerState>,
|
||||
_args: VolunteerRecruitArgs
|
||||
@@ -363,6 +368,14 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return DEFAULT_PRE_TURN;
|
||||
}
|
||||
|
||||
getPostReqTurn(context: VolunteerRecruitResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: VolunteerRecruitResolveContext<TriggerState>,
|
||||
args: VolunteerRecruitArgs
|
||||
|
||||
@@ -54,10 +54,19 @@ const resolveNextTerm = (state: number, term: number): number => (state === DIPL
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 16) * 10);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
@@ -70,8 +79,12 @@ export class ActionResolver<
|
||||
readonly key = 'che_이호경식';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||
return this.command.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -174,8 +187,8 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DegradeRelationsArgs | null {
|
||||
@@ -198,6 +211,10 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DegradeRelationsResolveContext<TriggerState>,
|
||||
args: DegradeRelationsArgs
|
||||
@@ -245,5 +262,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
reqArg: true,
|
||||
availabilityArgs: { destNationId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect, createMessageEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
@@ -27,12 +27,20 @@ const ARGS_SCHEMA = z.object({
|
||||
});
|
||||
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
interface StopWarProposalContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
messageValidMinutes: number;
|
||||
messageTime: Date;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '종전 제의';
|
||||
|
||||
// 종전 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, StopWarProposalArgs> {
|
||||
> implements GeneralActionDefinition<TriggerState, StopWarProposalArgs, StopWarProposalContext<TriggerState>> {
|
||||
public readonly key = 'che_종전제의';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -56,14 +64,41 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
resolve(
|
||||
_context: GeneralActionResolveContext<TriggerState>,
|
||||
args: StopWarProposalArgs
|
||||
context: StopWarProposalContext<TriggerState>,
|
||||
_args: StopWarProposalArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const destNationName =
|
||||
(_context as { destNation?: { name?: string } }).destNation?.name ?? `국가${args.destNationId}`;
|
||||
const { general, nation, destNation } = context;
|
||||
if (!nation) {
|
||||
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||
}
|
||||
const destNationName = destNation.name;
|
||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||
return {
|
||||
effects: [
|
||||
createMessageEffect({
|
||||
msgType: 'diplomacy',
|
||||
src: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: nation.id,
|
||||
nationName: nation.name,
|
||||
color: nation.color,
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: 0,
|
||||
generalName: '',
|
||||
nationId: destNation.id,
|
||||
nationName: destNation.name,
|
||||
color: destNation.color,
|
||||
icon: '',
|
||||
},
|
||||
text: `${nation.name}의 종전 제의 서신`,
|
||||
time: context.messageTime,
|
||||
validUntil,
|
||||
option: { action: 'stopWar', deletable: false },
|
||||
}),
|
||||
createLogEffect(`<D><b>${destNationName}</b></>${josaRo} 종전 제의 서신을 보냈습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
@@ -75,7 +110,18 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
||||
export const actionContextBuilder = defaultActionContextBuilder;
|
||||
export const actionContextBuilder: ActionContextBuilder<StopWarProposalArgs> = (base, options) => {
|
||||
const destNation = options.worldRef?.getNationById(options.actionArgs.destNationId);
|
||||
if (!destNation) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
messageTime: base.general.turnTime,
|
||||
messageValidMinutes: Math.max(30, Math.floor((options.world.tickSeconds / 60) * 3)),
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_종전제의',
|
||||
|
||||
@@ -88,6 +88,7 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, ExpandCityArgs, ExpandCityResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_증축';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
@@ -118,6 +119,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getStackSequence(context: ExpandCityResolveContext<TriggerState>): number {
|
||||
const value = context.nation?.meta.capset;
|
||||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: ExpandCityResolveContext<TriggerState>,
|
||||
_args: ExpandCityArgs
|
||||
@@ -153,6 +163,10 @@ export class ActionDefinition<
|
||||
{
|
||||
gold: nation.gold - cost,
|
||||
rice: nation.rice - cost,
|
||||
meta: {
|
||||
...nation.meta,
|
||||
capset: (typeof nation.meta.capset === 'number' ? nation.meta.capset : 0) + 1,
|
||||
},
|
||||
},
|
||||
nation.id
|
||||
),
|
||||
@@ -184,6 +198,11 @@ export class ActionDefinition<
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// General Action Log
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
|
||||
@@ -36,35 +36,43 @@ export interface MoveCapitalResolveContext<
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
map: MapDefinition;
|
||||
nationCities: City[];
|
||||
}
|
||||
|
||||
const ACTION_NAME = '천도';
|
||||
|
||||
const hasRouteToDestCity = (destCityID: number, develCost: number): Constraint => ({
|
||||
const hasRouteToDestCity = (destCityID: number): Constraint => ({
|
||||
name: 'hasRouteToDestCity',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }, { kind: 'env', key: 'map' }],
|
||||
requires: (ctx) => [
|
||||
{ kind: 'nation', id: ctx.nationId! },
|
||||
{ kind: 'env', key: 'map' },
|
||||
{ kind: 'env', key: 'cities' },
|
||||
],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined;
|
||||
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | undefined;
|
||||
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
const dist = calcDistance(nation.capitalCityId, destCityID, map);
|
||||
if (dist >= 50) {
|
||||
const allowedCityIds = new Set(
|
||||
(cities ?? []).filter((city) => city.nationId === nation.id).map((city) => city.id)
|
||||
);
|
||||
const dist = calcDistance(nation.capitalCityId, destCityID, map, allowedCityIds);
|
||||
if (dist === null) {
|
||||
return { kind: 'deny', reason: '천도 대상으로 도달할 방법이 없습니다.' };
|
||||
}
|
||||
const cost = develCost * 5 * Math.pow(2, dist);
|
||||
if (nation.gold < cost + 1000) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
if (nation.rice < cost + 1000) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
});
|
||||
|
||||
const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition): number => {
|
||||
const calcDistance = (
|
||||
fromCityId: number,
|
||||
toCityId: number,
|
||||
map: MapDefinition,
|
||||
allowedCityIds?: ReadonlySet<number>
|
||||
): number | null => {
|
||||
if (allowedCityIds && !allowedCityIds.has(toCityId)) return null;
|
||||
if (fromCityId === toCityId) return 0;
|
||||
|
||||
const connections = new Map<number, number[]>();
|
||||
@@ -82,6 +90,9 @@ const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition):
|
||||
|
||||
const nextNodes = connections.get(current) ?? [];
|
||||
for (const next of nextNodes) {
|
||||
if (allowedCityIds && !allowedCityIds.has(next)) {
|
||||
continue;
|
||||
}
|
||||
if (!visited.has(next)) {
|
||||
visited.add(next);
|
||||
queue.push([next, dist + 1]);
|
||||
@@ -89,7 +100,7 @@ const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition):
|
||||
}
|
||||
}
|
||||
|
||||
return 50;
|
||||
return null;
|
||||
};
|
||||
|
||||
export class ActionDefinition<
|
||||
@@ -97,6 +108,7 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, MoveCapitalArgs, MoveCapitalResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_천도';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
@@ -119,8 +131,12 @@ export class ActionDefinition<
|
||||
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||
return 0;
|
||||
}
|
||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
||||
if (dist >= 50) {
|
||||
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | undefined;
|
||||
const allowedCityIds = new Set(
|
||||
(cities ?? []).filter((city) => city.nationId === nation.id).map((city) => city.id)
|
||||
);
|
||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map, allowedCityIds);
|
||||
if (dist === null) {
|
||||
return 0;
|
||||
}
|
||||
return develcost * 5 * Math.pow(2, dist);
|
||||
@@ -132,13 +148,38 @@ export class ActionDefinition<
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
suppliedDestCity(),
|
||||
hasRouteToDestCity(args.destCityID, develcost),
|
||||
hasRouteToDestCity(args.destCityID),
|
||||
reqNationValue('capitalCityId', '수도', '!=', args.destCityID, '이미 수도입니다.'),
|
||||
reqNationGold((ctx, view) => baseGold + getRequiredCost(ctx, view), [{ kind: 'env', key: 'map' }]),
|
||||
reqNationRice((ctx, view) => baseRice + getRequiredCost(ctx, view), [{ kind: 'env', key: 'map' }]),
|
||||
reqNationGold(
|
||||
(ctx, view) => baseGold + getRequiredCost(ctx, view),
|
||||
[
|
||||
{ kind: 'env', key: 'map' },
|
||||
{ kind: 'env', key: 'cities' },
|
||||
]
|
||||
),
|
||||
reqNationRice(
|
||||
(ctx, view) => baseRice + getRequiredCost(ctx, view),
|
||||
[
|
||||
{ kind: 'env', key: 'map' },
|
||||
{ kind: 'env', key: 'cities' },
|
||||
]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(context: MoveCapitalResolveContext<TriggerState>, args: MoveCapitalArgs): number {
|
||||
if (!context.nation?.capitalCityId) {
|
||||
return 0;
|
||||
}
|
||||
const allowedCityIds = new Set(context.nationCities.map((city) => city.id));
|
||||
return (calcDistance(context.nation.capitalCityId, args.destCityID, context.map, allowedCityIds) ?? 0) * 2;
|
||||
}
|
||||
|
||||
getStackSequence(context: MoveCapitalResolveContext<TriggerState>): number {
|
||||
const value = context.nation?.meta.capset;
|
||||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: MoveCapitalResolveContext<TriggerState>,
|
||||
args: MoveCapitalArgs
|
||||
@@ -148,9 +189,12 @@ export class ActionDefinition<
|
||||
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||
}
|
||||
|
||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
||||
const cost = this.env.develCost * 5 * Math.pow(2, dist);
|
||||
|
||||
const nationCities = context.nationCities ?? [];
|
||||
const allowedCityIds = nationCities.length > 0 ? new Set(nationCities.map((city) => city.id)) : undefined;
|
||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map, allowedCityIds);
|
||||
if (dist === null) {
|
||||
return { effects: [createLogEffect('천도 대상으로 도달할 방법이 없습니다.', { scope: LogScope.GENERAL })] };
|
||||
}
|
||||
const generalName = general.name;
|
||||
const nationName = nation.name;
|
||||
const destCityName = destCity.name;
|
||||
@@ -163,8 +207,11 @@ export class ActionDefinition<
|
||||
createNationPatchEffect(
|
||||
{
|
||||
capitalCityId: args.destCityID,
|
||||
gold: nation.gold - cost,
|
||||
rice: nation.rice - cost,
|
||||
// ref는 비용 보유를 제약에서 검사하지만 실행 시 차감하지 않는다.
|
||||
meta: {
|
||||
...nation.meta,
|
||||
capset: (typeof nation.meta.capset === 'number' ? nation.meta.capset : 0) + 1,
|
||||
},
|
||||
},
|
||||
nation.id
|
||||
),
|
||||
@@ -196,6 +243,11 @@ export class ActionDefinition<
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>명령`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// General Action Log
|
||||
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} ${ACTION_NAME}했습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
@@ -226,6 +278,7 @@ export const actionContextBuilder: ActionContextBuilder<MoveCapitalArgs> = (base
|
||||
...base,
|
||||
destCity,
|
||||
map,
|
||||
nationCities: worldRef.listCities().filter((city) => city.nationId === base.general.nationId),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, ScorchedEarthArgs, ScorchedEarthResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_초토화';
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
parseArgs(raw: unknown): ScorchedEarthArgs | null {
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
@@ -102,6 +103,10 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return PRE_REQ_TURN;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: ScorchedEarthResolveContext<TriggerState>,
|
||||
_args: ScorchedEarthArgs
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
@@ -15,7 +16,7 @@ import type {
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
@@ -32,10 +33,12 @@ export interface CounterStrategyResolveContext<
|
||||
destNation: Nation;
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
destNationGenerals: Array<General<TriggerState>>;
|
||||
currentYearMonth: number;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '피장파장';
|
||||
const DEFAULT_GLOBAL_DELAY = 8;
|
||||
const TARGET_DELAY = 60;
|
||||
const PRE_REQ_TURN = 1;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
|
||||
@@ -89,11 +92,53 @@ const reqValidStrategicCommandType = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
const alwaysFail = (commandType: CounterStrategyArgs['commandType']): Constraint => ({
|
||||
// Legacy inserts AlwaysFail when the selected strategy is still cooling down.
|
||||
name: 'alwaysFail',
|
||||
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
|
||||
test: (ctx, view) => {
|
||||
if (ctx.nationId === undefined) {
|
||||
return { kind: 'deny', reason: '국가 정보가 없습니다.' };
|
||||
}
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined;
|
||||
if (!nation) {
|
||||
return { kind: 'deny', reason: '국가 정보가 없습니다.' };
|
||||
}
|
||||
const raw = nation.meta[`next_execute_${STRATEGIC_COMMANDS[commandType]}`];
|
||||
const nextAvailable = typeof raw === 'number' ? raw : Number(raw ?? 0);
|
||||
const year = Number(ctx.env.currentYear ?? ctx.env.year);
|
||||
const month = Number(ctx.env.currentMonth ?? ctx.env.month);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month)) {
|
||||
return { kind: 'unknown', missing: [{ kind: 'env', key: 'currentYear' }] };
|
||||
}
|
||||
const currentYearMonth = Math.floor(year) * 12 + Math.floor(month) - 1;
|
||||
if (Number.isFinite(nextAvailable) && nextAvailable > currentYearMonth) {
|
||||
return { kind: 'deny', reason: '해당 전략을 아직 사용할 수 없습니다' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
// 피장파장 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, CounterStrategyArgs> {
|
||||
readonly key = 'che_피장파장';
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined> = [],
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getTargetPostReqTurn(context: CounterStrategyResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 2) * 10);
|
||||
const triggered = Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
return Math.max(triggered, Math.round(TARGET_DELAY * 1.2));
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: CounterStrategyResolveContext<TriggerState>,
|
||||
@@ -105,6 +150,7 @@ export class ActionResolver<
|
||||
const nationName = nation?.name ?? '아국';
|
||||
const destNationName = destNation.name;
|
||||
const targetCommandName = STRATEGIC_COMMANDS[args.commandType] ?? args.commandType;
|
||||
const currentYearMonth = Number.isFinite(context.currentYearMonth) ? context.currentYearMonth : 0;
|
||||
const actionName = ACTION_NAME;
|
||||
const actionJosa = JosaUtil.pick(actionName, '을');
|
||||
|
||||
@@ -117,8 +163,8 @@ export class ActionResolver<
|
||||
context.addLog(
|
||||
`<D><b>${destNationName}</b></>에 <G><b>${targetCommandName}</b></> <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||
{
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -159,6 +205,7 @@ export class ActionResolver<
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
[`next_execute_${targetCommandName}`]: currentYearMonth + this.getTargetPostReqTurn(context),
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
@@ -170,6 +217,22 @@ export class ActionResolver<
|
||||
);
|
||||
}
|
||||
|
||||
const destMeta = destNation.meta;
|
||||
const destKey = `next_execute_${targetCommandName}`;
|
||||
const destRaw = destMeta[destKey];
|
||||
const destNext = typeof destRaw === 'number' ? destRaw : Number(destRaw ?? 0);
|
||||
effects.push(
|
||||
createNationPatchEffect(
|
||||
{
|
||||
meta: {
|
||||
...destMeta,
|
||||
[destKey]: Math.max(Number.isFinite(destNext) ? destNext : 0, currentYearMonth) + TARGET_DELAY,
|
||||
},
|
||||
},
|
||||
destNation.id
|
||||
)
|
||||
);
|
||||
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<D><b>${nationName}</b></>의 <Y>${generalName}</>${generalJosa} 아국에 <G><b>${targetCommandName}</b></> <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||
@@ -206,7 +269,11 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, CounterStrategyArgs, CounterStrategyResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_피장파장';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver = new ActionResolver<TriggerState>();
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined> = [], initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): CounterStrategyArgs | null {
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
@@ -226,10 +293,22 @@ export class ActionDefinition<
|
||||
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
|
||||
availableStrategicCommand(),
|
||||
reqValidStrategicCommandType(),
|
||||
alwaysFail(_args.commandType),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(context: CounterStrategyResolveContext<TriggerState>, args: CounterStrategyArgs): GeneralActionOutcome<TriggerState> {
|
||||
getPreReqTurn(): number {
|
||||
return PRE_REQ_TURN;
|
||||
}
|
||||
|
||||
getPostReqTurn(): number {
|
||||
return 8;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: CounterStrategyResolveContext<TriggerState>,
|
||||
args: CounterStrategyArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -256,6 +335,7 @@ export const actionContextBuilder: ActionContextBuilder<CounterStrategyArgs> = (
|
||||
destNation,
|
||||
friendlyGenerals,
|
||||
destNationGenerals,
|
||||
currentYearMonth: options.world.currentYear * 12 + options.world.currentMonth - 1,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -265,5 +345,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
reqArg: true,
|
||||
availabilityArgs: { destNationId: 0, commandType: '' },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -40,10 +40,19 @@ const ATMOS_CAP = 100;
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DesperateFightResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.nationGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 8) * 10);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DesperateFightResolveContext<TriggerState>): number {
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
@@ -56,8 +65,12 @@ export class ActionResolver<
|
||||
readonly key = 'che_필사즉생';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DesperateFightResolveContext<TriggerState>): number {
|
||||
return this.command.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -142,8 +155,8 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(_raw: unknown): DesperateFightArgs | null {
|
||||
@@ -162,6 +175,14 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return PRE_REQ_TURN;
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DesperateFightResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DesperateFightResolveContext<TriggerState>,
|
||||
args: DesperateFightArgs
|
||||
@@ -188,5 +209,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: false,
|
||||
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -65,10 +65,19 @@ const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: num
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
private readonly initialNationGenLimit = 10
|
||||
) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DeceptionResolveContext<TriggerState>): number {
|
||||
const genCount = Math.max(context.friendlyGenerals.length, this.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(genCount * 4) * 10);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DeceptionResolveContext<TriggerState>): number {
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
@@ -81,8 +90,12 @@ export class ActionResolver<
|
||||
readonly key = 'che_허보';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.command = new CommandResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DeceptionResolveContext<TriggerState>): number {
|
||||
return this.command.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(context: DeceptionResolveContext<TriggerState>, _args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
||||
@@ -176,8 +189,8 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
|
||||
this.resolver = new ActionResolver(modules, initialNationGenLimit);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DeceptionArgs | null {
|
||||
@@ -201,6 +214,14 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return PRE_REQ_TURN;
|
||||
}
|
||||
|
||||
getPostReqTurn(context: DeceptionResolveContext<TriggerState>): number {
|
||||
return this.resolver.getPostReqTurn(context);
|
||||
}
|
||||
|
||||
resolve(context: DeceptionResolveContext<TriggerState>, args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
@@ -245,5 +266,6 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
reqArg: true,
|
||||
availabilityArgs: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env.initialNationGenLimit),
|
||||
};
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/c
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import { beChief, occupiedCity, reqNationGold, reqNationRice } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
@@ -43,7 +40,9 @@ const reqNationAuxValue = (auxKey: string, actionName: string): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const createEventResearchCommand = (config: EventResearchConfig): {
|
||||
export const createEventResearchCommand = (
|
||||
config: EventResearchConfig
|
||||
): {
|
||||
ActionDefinition: new <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
env: TurnCommandEnv
|
||||
) => GeneralActionDefinition<TriggerState, Record<string, never>>;
|
||||
@@ -61,6 +60,7 @@ export const createEventResearchCommand = (config: EventResearchConfig): {
|
||||
> implements GeneralActionDefinition<TriggerState, Record<string, never>> {
|
||||
public readonly key = config.key;
|
||||
public readonly name = ACTION_NAME;
|
||||
public readonly countsAsInheritanceActiveAction = true;
|
||||
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
@@ -93,6 +93,10 @@ export const createEventResearchCommand = (config: EventResearchConfig): {
|
||||
];
|
||||
}
|
||||
|
||||
getPreReqTurn(): number {
|
||||
return PRE_REQ_TURN;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
_args: Record<string, never>
|
||||
|
||||
@@ -21,20 +21,15 @@ export type TriggerDomesticActionType =
|
||||
|
||||
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';
|
||||
|
||||
export type TriggerStrategicActionType = '의병모집' | '허보' | '필사즉생' | '백성동원' | '이호경식' | '수몰' | '급습';
|
||||
export type TriggerStrategicActionType =
|
||||
'의병모집' | '허보' | '필사즉생' | '백성동원' | '이호경식' | '수몰' | '급습' | '피장파장';
|
||||
|
||||
export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
||||
|
||||
export type TriggerNationalIncomeType = 'gold' | 'rice' | 'pop';
|
||||
|
||||
export type GeneralStatName =
|
||||
| 'leadership'
|
||||
| 'strength'
|
||||
| 'intelligence'
|
||||
| 'experience'
|
||||
| 'dedication'
|
||||
| 'sabotageDefence'
|
||||
| 'sabotageAttack';
|
||||
'leadership' | 'strength' | 'intelligence' | 'experience' | 'dedication' | 'sabotageDefence' | 'sabotageAttack';
|
||||
|
||||
export type WarStatName =
|
||||
| GeneralStatName
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js';
|
||||
import { ActionDefinition as StopWarProposalAction } from '../../../src/actions/turn/nation/che_종전제의.js';
|
||||
import { ActionDefinition as ScorchedEarthAction } from '../../../src/actions/turn/nation/che_초토화.js';
|
||||
import { ActionDefinition as CounterStrategyAction } from '../../../src/actions/turn/nation/che_피장파장.js';
|
||||
import { ActionDefinition as MaterialAidAction } from '../../../src/actions/turn/nation/che_물자원조.js';
|
||||
import { ActionDefinition as PopulationMoveAction } from '../../../src/actions/turn/nation/cr_인구이동.js';
|
||||
import { ActionDefinition as EventWonyungAction } from '../../../src/actions/turn/nation/event_원융노병연구.js';
|
||||
import { ActionDefinition as EventHwasibyeongAction } from '../../../src/actions/turn/nation/event_화시병연구.js';
|
||||
@@ -205,13 +206,7 @@ const buildEnv = (): TurnCommandEnv => ({
|
||||
maxResourceActionAmount: 100000,
|
||||
});
|
||||
|
||||
const setupDiplomacy = (
|
||||
view: TestStateView,
|
||||
srcNationId: number,
|
||||
destNationId: number,
|
||||
state: number,
|
||||
term = 0
|
||||
) => {
|
||||
const setupDiplomacy = (view: TestStateView, srcNationId: number, destNationId: number, state: number, term = 0) => {
|
||||
view.set(
|
||||
{
|
||||
kind: 'diplomacy',
|
||||
@@ -267,6 +262,9 @@ describe('Nation Missing Actions', () => {
|
||||
general,
|
||||
city,
|
||||
nation,
|
||||
destNation,
|
||||
messageTime: new Date('2026-01-01T00:00:00Z'),
|
||||
messageValidMinutes: 30,
|
||||
rng: {} as any,
|
||||
addLog: () => {},
|
||||
} as any,
|
||||
@@ -275,6 +273,15 @@ describe('Nation Missing Actions', () => {
|
||||
);
|
||||
|
||||
expect(resolution.logs.some((log) => log.text.includes('종전 제의'))).toBe(true);
|
||||
expect(resolution.effects).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'message:add',
|
||||
draft: expect.objectContaining({
|
||||
msgType: 'diplomacy',
|
||||
option: { action: 'stopWar', deletable: false },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('che_피장파장: blocks when not at war/declare', () => {
|
||||
@@ -323,6 +330,7 @@ describe('Nation Missing Actions', () => {
|
||||
destNation,
|
||||
friendlyGenerals: [general, otherGeneral],
|
||||
destNationGenerals: [enemyGeneral],
|
||||
currentYearMonth: 155,
|
||||
rng: {} as any,
|
||||
addLog: () => {},
|
||||
} as any,
|
||||
@@ -332,6 +340,54 @@ describe('Nation Missing Actions', () => {
|
||||
|
||||
expect(resolution.general.experience).toBeGreaterThan(100);
|
||||
expect(resolution.nation?.meta?.strategic_cmd_limit).toBe(8);
|
||||
expect(resolution.nation?.meta?.next_execute_허보).toBe(227);
|
||||
expect(resolution.patches?.nations).toContainEqual({
|
||||
id: destNation.id,
|
||||
patch: expect.objectContaining({
|
||||
meta: expect.objectContaining({ next_execute_허보: 215 }),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('che_물자원조: validates amounts and records the legacy receive-assist accumulator', () => {
|
||||
const definition = new MaterialAidAction(buildEnv());
|
||||
expect(definition.parseArgs({ destNationId: 2, amountList: [0, 0] })).toBeNull();
|
||||
expect(definition.parseArgs({ destNationId: 2, amountList: [-1, 10] })).toBeNull();
|
||||
|
||||
const general = buildGeneral(1, 1, 1);
|
||||
const nation = { ...buildNation(1), gold: 1000, rice: 1000 };
|
||||
const destNation = { ...buildNation(2), gold: 100, rice: 100 };
|
||||
const resolution = resolveGeneralAction(
|
||||
definition,
|
||||
{
|
||||
general,
|
||||
city: buildCity(1, 1),
|
||||
nation,
|
||||
destNation,
|
||||
friendlyChiefs: [general],
|
||||
destNationChiefs: [],
|
||||
rng: {} as any,
|
||||
addLog: () => {},
|
||||
} as any,
|
||||
{ now: new Date(), schedule },
|
||||
{ destNationId: 2, amountList: [100, 50] }
|
||||
);
|
||||
|
||||
expect(resolution.nation).toMatchObject({
|
||||
gold: 900,
|
||||
rice: 950,
|
||||
meta: { surlimit: 12 },
|
||||
});
|
||||
expect(resolution.patches?.nations).toContainEqual({
|
||||
id: 2,
|
||||
patch: expect.objectContaining({
|
||||
gold: 200,
|
||||
rice: 150,
|
||||
meta: expect.objectContaining({
|
||||
recv_assist: { n1: { 0: 1, 1: 150 } },
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('che_초토화: blocks when diplomacy limit exists', () => {
|
||||
|
||||
Reference in New Issue
Block a user