fix nation diplomacy proposal parity
This commit is contained in:
@@ -20,15 +20,9 @@ import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
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)
|
||||
),
|
||||
destNationId: z.number().int().positive(),
|
||||
year: z.number().int().min(0),
|
||||
month: z.number().int().min(1).max(12),
|
||||
});
|
||||
export type NonAggressionProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
@@ -52,12 +46,14 @@ const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
||||
{ kind: 'arg', key: 'month' },
|
||||
{ kind: 'env', key: 'year' },
|
||||
{ kind: 'env', key: 'month' },
|
||||
{ kind: 'env', key: 'startYear' },
|
||||
],
|
||||
test: (ctx) => {
|
||||
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
||||
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
||||
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const startYearValue = typeof ctx.env.startYear === 'number' ? ctx.env.startYear : null;
|
||||
const missing = [];
|
||||
|
||||
if (yearValue === null) {
|
||||
@@ -72,17 +68,27 @@ const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
||||
if (envMonthValue === null) {
|
||||
missing.push({ kind: 'env', key: 'month' } as const);
|
||||
}
|
||||
if (startYearValue === null) {
|
||||
missing.push({ kind: 'env', key: 'startYear' } as const);
|
||||
}
|
||||
|
||||
if (
|
||||
missing.length > 0 ||
|
||||
yearValue === null ||
|
||||
monthValue === null ||
|
||||
envYearValue === null ||
|
||||
envMonthValue === null
|
||||
envMonthValue === null ||
|
||||
startYearValue === null
|
||||
) {
|
||||
return unknownOrDeny(ctx, missing, '기한 정보가 없습니다.');
|
||||
}
|
||||
|
||||
if (yearValue < startYearValue) {
|
||||
return {
|
||||
kind: 'deny',
|
||||
reason: '시작 연도보다 이전의 기한은 지정할 수 없습니다.',
|
||||
};
|
||||
}
|
||||
const currentMonth = resolveMonthIndex(envYearValue, envMonthValue);
|
||||
const targetMonth = resolveMonthIndex(yearValue, monthValue);
|
||||
if (targetMonth < currentMonth + minMonths) {
|
||||
@@ -137,7 +143,7 @@ export class ActionDefinition<
|
||||
return { effects: [createLogEffect('국가 정보가 없습니다.')] };
|
||||
}
|
||||
const destNationName = destNation.name;
|
||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||
const josaRo = JosaUtil.pick(nation.name, '로');
|
||||
const josaWa = JosaUtil.pick(nation.name, '와');
|
||||
const validUntil = new Date(context.messageTime.getTime() + context.messageValidMinutes * 60_000);
|
||||
return {
|
||||
|
||||
@@ -20,10 +20,7 @@ import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
destNationId: z.number().int().positive(),
|
||||
});
|
||||
export type NonAggressionCancelProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createDiplomacyPatchEffect, createLogEffect, createMessageEffect } 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';
|
||||
@@ -25,14 +25,18 @@ import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
destNationId: 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
|
||||
interface DeclareWarResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
currentYear: number;
|
||||
currentMonth: number;
|
||||
messageTime: Date;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '선전포고';
|
||||
// legacy 규칙: 선전포고 상태는 24턴 유지.
|
||||
@@ -41,11 +45,7 @@ const DECLARE_TERM = 24;
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DeclareWarArgs,
|
||||
GeneralActionResolveContext<TriggerState> & { destNation: Nation }
|
||||
> {
|
||||
> implements GeneralActionDefinition<TriggerState, DeclareWarArgs, DeclareWarResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_선전포고';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -98,10 +98,7 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState> & { destNation: Nation },
|
||||
args: DeclareWarArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: DeclareWarResolveContext<TriggerState>, args: DeclareWarArgs): GeneralActionOutcome<TriggerState> {
|
||||
const nationId = context.nation?.id;
|
||||
if (nationId === undefined || nationId <= 0 || !context.destNation) {
|
||||
return {
|
||||
@@ -157,27 +154,46 @@ export class ActionDefinition<
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// Global Action Log
|
||||
createLogEffect(`<Y>${generalName}</>${josaYiGeneral} <D><b>${destNationName}</b></>에 <M>선전 포고</> 하였습니다.`, {
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
}),
|
||||
// Global History Log
|
||||
createLogEffect(`<R><b>【선포】</b></><D><b>${nationName}</b></>${josaYiNation} <D><b>${destNationName}</b></>에 선전 포고 하였습니다.`, {
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}),
|
||||
// National Message (국메)
|
||||
createLogEffect(
|
||||
`【국메】<Y>${generalName}</>${josaYiGeneral} <Y>${destNationName}</>에게 <R>${ACTION_NAME}</>하였습니다!`,
|
||||
`<Y>${generalName}</>${josaYiGeneral} <D><b>${destNationName}</b></>에 <M>선전 포고</> 하였습니다.`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
nationId: nationId,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.SUMMARY,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
// Global History Log
|
||||
createLogEffect(
|
||||
`<R><b>【선포】</b></><D><b>${nationName}</b></>${josaYiNation} <D><b>${destNationName}</b></>에 선전 포고 하였습니다.`,
|
||||
{
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
),
|
||||
createMessageEffect({
|
||||
msgType: 'national',
|
||||
src: {
|
||||
generalId: context.general.id,
|
||||
generalName,
|
||||
nationId,
|
||||
nationName,
|
||||
color: context.nation?.color ?? '',
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: 0,
|
||||
generalName: '',
|
||||
nationId: args.destNationId,
|
||||
nationName: destNationName,
|
||||
color: context.destNation.color,
|
||||
icon: '',
|
||||
},
|
||||
text: `【외교】${context.currentYear}년 ${context.currentMonth}월:${nationName}에서 ${destNationName}에 선전포고`,
|
||||
time: context.messageTime,
|
||||
validUntil: new Date('9999-12-31T00:00:00.000Z'),
|
||||
option: {},
|
||||
}),
|
||||
];
|
||||
|
||||
return { effects };
|
||||
@@ -201,6 +217,9 @@ export const actionContextBuilder: ActionContextBuilder<DeclareWarArgs> = (base,
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
currentYear: options.world.currentYear,
|
||||
currentMonth: options.world.currentMonth,
|
||||
messageTime: base.general.turnTime,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -20,10 +20,7 @@ import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destNationId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
destNationId: z.number().int().positive(),
|
||||
});
|
||||
export type StopWarProposalArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { describe, expect, it } from 'vitest';
|
||||
import type { City, General, Nation } from '../../../src/domain/entities.js';
|
||||
import { resolveGeneralAction } from '../../../src/actions/engine.js';
|
||||
import { ActionDefinition as DeclareWarAction } from '../../../src/actions/turn/nation/che_선전포고.js';
|
||||
import { ActionDefinition as NonAggressionProposalAction } from '../../../src/actions/turn/nation/che_불가침제의.js';
|
||||
import { ActionDefinition as StopWarProposalAction } from '../../../src/actions/turn/nation/che_종전제의.js';
|
||||
import { ActionDefinition as NonAggressionCancelProposalAction } from '../../../src/actions/turn/nation/che_불가침파기제의.js';
|
||||
import { ActionDefinition as MoveCapitalAction } from '../../../src/actions/turn/nation/che_천도.js';
|
||||
import {
|
||||
ActionDefinition as ChangeNationNameAction,
|
||||
@@ -125,6 +128,9 @@ describe('Nation Actions', () => {
|
||||
destNation: nation2,
|
||||
cities: [city1, city2],
|
||||
nations: [nation1, nation2],
|
||||
currentYear: 190,
|
||||
currentMonth: 1,
|
||||
messageTime: new Date('2026-01-01T00:00:00Z'),
|
||||
rng: {} as any,
|
||||
addLog: () => {},
|
||||
};
|
||||
@@ -144,10 +150,19 @@ describe('Nation Actions', () => {
|
||||
patch: expect.objectContaining({ state: 1 }),
|
||||
})
|
||||
);
|
||||
expect(resolution.logs.some((l) => l.scope === LogScope.SYSTEM && l.category === LogCategory.ACTION)).toBe(
|
||||
expect(resolution.logs.some((l) => l.scope === LogScope.SYSTEM && l.category === LogCategory.SUMMARY)).toBe(
|
||||
true
|
||||
);
|
||||
expect(resolution.logs.some((l) => l.text.includes('【국메】'))).toBe(true);
|
||||
expect(resolution.effects).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'message:add',
|
||||
draft: expect.objectContaining({
|
||||
msgType: 'national',
|
||||
text: '【외교】190년 1월:Nation1에서 Nation2에 선전포고',
|
||||
option: {},
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if not neighbor', () => {
|
||||
@@ -234,6 +249,23 @@ describe('Nation Actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('diplomacy proposal argument boundaries', () => {
|
||||
it.each([
|
||||
['che_선전포고', new DeclareWarAction(), { destNationId: 2.9 }],
|
||||
[
|
||||
'che_불가침제의 destination',
|
||||
new NonAggressionProposalAction(),
|
||||
{ destNationId: 2.9, year: 190, month: 7 },
|
||||
],
|
||||
['che_불가침제의 year', new NonAggressionProposalAction(), { destNationId: 2, year: 190.9, month: 7 }],
|
||||
['che_불가침제의 month', new NonAggressionProposalAction(), { destNationId: 2, year: 190, month: 7.9 }],
|
||||
['che_종전제의', new StopWarProposalAction(), { destNationId: 2.9 }],
|
||||
['che_불가침파기제의', new NonAggressionCancelProposalAction(), { destNationId: 2.9 }],
|
||||
])('%s rejects fractional numeric arguments', (_name, definition, args) => {
|
||||
expect(definition.parseArgs(args)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('che_필사즉생 (Last Stand)', () => {
|
||||
it('applies the legacy three-turn gains and global delay', () => {
|
||||
const nation = buildNation(1);
|
||||
|
||||
Reference in New Issue
Block a user