feat: add non-aggression cancel actions and related diplomacy constraints

This commit is contained in:
2026-01-03 06:09:10 +00:00
parent 11ffdfef0e
commit b628b32968
6 changed files with 302 additions and 1 deletions
@@ -0,0 +1,140 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
import {
allowDiplomacyBetweenStatus,
beChief,
destGeneralInDestNation,
existsDestGeneral,
existsDestNation,
notBeNeutral,
} from '../../../constraints/presets.js';
import { allow, unknownOrDeny } from '../../../constraints/helpers.js';
import type { GeneralActionDefinition } from '../../definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createDiplomacyPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
export interface NonAggressionCancelAcceptArgs {
destNationId: number;
destGeneralId: number;
}
const ACTION_NAME = '불가침 파기 수락';
const DIPLOMACY_NEUTRAL = 2;
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;
};
const parseGeneralId = (raw: unknown): number | null => {
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
return null;
}
return raw > 0 ? Math.floor(raw) : null;
};
const notSameDestGeneral = (): Constraint => ({
name: 'NotSameDestGeneral',
requires: () => [{ kind: 'arg', key: 'destGeneralId' }],
test: (ctx) => {
const destGeneralId = ctx.args.destGeneralId;
if (typeof destGeneralId !== 'number') {
return unknownOrDeny(
ctx,
[{ kind: 'arg', key: 'destGeneralId' }],
'장수 정보가 없습니다.'
);
}
if (destGeneralId === ctx.actorId) {
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
}
return allow();
},
});
// 불가침 파기 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements
GeneralActionDefinition<TriggerState, NonAggressionCancelAcceptArgs>
{
public readonly key = 'che_불가침파기수락';
public readonly name = ACTION_NAME;
parseArgs(raw: unknown): NonAggressionCancelAcceptArgs | null {
const data = raw as { destNationId?: unknown; destGeneralId?: unknown };
const destNationId = parseNationId(data?.destNationId);
const destGeneralId = parseGeneralId(data?.destGeneralId);
if (destNationId === null || destGeneralId === null) {
return null;
}
return { destNationId, destGeneralId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: NonAggressionCancelAcceptArgs
): Constraint[] {
return [
beChief(),
notBeNeutral(),
existsDestNation(),
existsDestGeneral(),
destGeneralInDestNation(),
notSameDestGeneral(),
allowDiplomacyBetweenStatus(
[DIPLOMACY_NON_AGGRESSION],
'불가침 중인 상대국에게만 가능합니다.'
),
];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
args: NonAggressionCancelAcceptArgs
): GeneralActionOutcome<TriggerState> {
const nationId = context.nation?.id;
if (nationId === undefined || nationId <= 0) {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
],
};
}
return {
effects: [
createDiplomacyPatchEffect(nationId, args.destNationId, {
state: DIPLOMACY_NEUTRAL,
term: 0,
}),
createDiplomacyPatchEffect(args.destNationId, nationId, {
state: DIPLOMACY_NEUTRAL,
term: 0,
}),
createLogEffect(
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
],
};
}
}
@@ -1 +1,9 @@
export * from './che_불가침수락.js';
export {
ActionDefinition as NonAggressionAcceptActionDefinition,
type NonAggressionAcceptArgs,
type NonAggressionAcceptContext,
} from './che_불가침수락.js';
export {
ActionDefinition as NonAggressionCancelAcceptActionDefinition,
type NonAggressionCancelAcceptArgs,
} from './che_불가침파기수락.js';
@@ -0,0 +1,95 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
import {
allowDiplomacyBetweenStatus,
beChief,
existsDestNation,
notBeNeutral,
occupiedCity,
suppliedCity,
} from '../../../constraints/presets.js';
import type { GeneralActionDefinition } from '../../definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
export interface NonAggressionCancelProposalArgs {
destNationId: number;
}
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
> implements
GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs>
{
public readonly key = 'che_불가침파기제의';
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 };
}
buildConstraints(
_ctx: ConstraintContext,
_args: NonAggressionCancelProposalArgs
): Constraint[] {
return [
beChief(),
notBeNeutral(),
occupiedCity(),
suppliedCity(),
existsDestNation(),
allowDiplomacyBetweenStatus(
[DIPLOMACY_NON_AGGRESSION],
'불가침 중인 상대국에게만 가능합니다.'
),
];
}
resolve(
_context: GeneralActionResolveContext<TriggerState>,
args: NonAggressionCancelProposalArgs
): GeneralActionOutcome<TriggerState> {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
],
};
}
}
export const commandSpec: NationTurnCommandSpec = {
key: 'che_불가침파기제의',
category: '외교',
reqArg: true,
args: { destNationId: 0 },
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
};
@@ -6,6 +6,7 @@ export const NATION_TURN_COMMAND_KEYS = [
'che_발령',
'che_선전포고',
'che_불가침제의',
'che_불가침파기제의',
'che_의병모집',
] as const;
@@ -29,6 +30,7 @@ const defaultImporters: Record<
che_발령: async () => import('./che_발령.js'),
che_선전포고: async () => import('./che_선전포고.js'),
che_불가침제의: async () => import('./che_불가침제의.js'),
che_불가침파기제의: async () => import('./che_불가침파기제의.js'),
che_의병모집: async () => import('./che_의병모집.js'),
};
@@ -63,3 +63,58 @@ export const disallowDiplomacyBetweenStatus = (
return allow();
},
});
export const allowDiplomacyBetweenStatus = (
allowList: number[],
reason: string
): Constraint => ({
name: 'AllowDiplomacyBetweenStatus',
requires: (ctx) => {
const reqs: RequirementKey[] = [];
if (ctx.nationId !== undefined) {
reqs.push({ kind: 'nation', id: ctx.nationId });
}
const destNationId = resolveDestNationId(ctx);
if (destNationId !== undefined) {
reqs.push({ kind: 'destNation', id: destNationId });
if (ctx.nationId !== undefined) {
reqs.push({
kind: 'diplomacy',
srcNationId: ctx.nationId,
destNationId,
});
}
}
const destCityId = resolveDestCityId(ctx);
if (destCityId !== undefined) {
reqs.push({ kind: 'destCity', id: destCityId });
}
return reqs;
},
test: (ctx, view) => {
const general = readGeneral(ctx, view);
const baseNationId = ctx.nationId ?? general?.nationId;
if (baseNationId === undefined) {
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
const destCity = readDestCity(ctx, view);
const destNationId =
resolveDestNationId(ctx) ?? destCity?.nationId;
if (destNationId === undefined) {
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
}
const state = readDiplomacyState(view, baseNationId, destNationId);
if (state === null) {
const req: RequirementKey = {
kind: 'diplomacy',
srcNationId: baseNationId,
destNationId,
};
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
}
if (!allowList.includes(state)) {
return { kind: 'deny', reason };
}
return allow();
},
});
+1
View File
@@ -25,6 +25,7 @@
"che_발령",
"che_선전포고",
"che_불가침제의",
"che_불가침파기제의",
"che_의병모집"
]
}