건국 커맨드 구현 및 테스트 추가
This commit is contained in:
@@ -91,6 +91,7 @@ export type GeneralActionEffect<TriggerState extends GeneralTriggerState = Gener
|
||||
| GeneralAddEffect<TriggerState>
|
||||
| CityPatchEffect
|
||||
| NationPatchEffect
|
||||
| NationAddEffect
|
||||
| DiplomacyPatchEffect
|
||||
| LogEffect
|
||||
| NextTurnOverrideEffect;
|
||||
@@ -117,6 +118,7 @@ export interface GeneralActionResolution {
|
||||
effects: GeneralActionEffect[];
|
||||
created?: {
|
||||
generals: General[];
|
||||
nations?: Nation[];
|
||||
};
|
||||
patches?: {
|
||||
generals: Array<{ id: GeneralId; patch: Partial<General> }>;
|
||||
@@ -165,6 +167,16 @@ export const createNationPatchEffect = (patch: Partial<Nation>, targetId?: Natio
|
||||
...(targetId !== undefined ? { targetId } : {}),
|
||||
});
|
||||
|
||||
export interface NationAddEffect {
|
||||
type: 'nation:add';
|
||||
nation: Nation;
|
||||
}
|
||||
|
||||
export const createNationAddEffect = (nation: Nation): NationAddEffect => ({
|
||||
type: 'nation:add',
|
||||
nation,
|
||||
});
|
||||
|
||||
export const createDiplomacyPatchEffect = (
|
||||
srcNationId: NationId,
|
||||
destNationId: NationId,
|
||||
@@ -206,6 +218,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
|
||||
const logs: LogEntryDraft[] = [];
|
||||
let nextTurnAtOverride: Date | null = null;
|
||||
const createdGenerals: General[] = [];
|
||||
const createdNations: Nation[] = [];
|
||||
const patches: NonNullable<GeneralActionResolution['patches']> = {
|
||||
generals: [],
|
||||
cities: [],
|
||||
@@ -284,6 +297,9 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
|
||||
case 'general:add':
|
||||
createdGenerals.push(effect.general as General);
|
||||
break;
|
||||
case 'nation:add':
|
||||
createdNations.push(effect.nation as Nation);
|
||||
break;
|
||||
case 'diplomacy:patch':
|
||||
pendingEffects.push(effect);
|
||||
break;
|
||||
@@ -366,9 +382,10 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
|
||||
if (patches.generals.length > 0 || patches.cities.length > 0 || patches.nations.length > 0) {
|
||||
resolution.patches = patches;
|
||||
}
|
||||
if (createdGenerals.length > 0) {
|
||||
if (createdGenerals.length > 0 || createdNations.length > 0) {
|
||||
resolution.created = {
|
||||
generals: createdGenerals,
|
||||
...(createdNations.length > 0 ? { nations: createdNations } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface ActionContextOptions {
|
||||
worldRef: ActionContextWorldRef | null;
|
||||
actionArgs: Record<string, unknown>;
|
||||
createGeneralId: () => number;
|
||||
createNationId: () => number;
|
||||
seedBase: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import type { GeneralTriggerState, TriggerValue } 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 { beNeutral } 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 { createGeneralPatchEffect, createNationAddEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat } 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, ActionContextBase } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface UprisingArgs {}
|
||||
|
||||
export interface UprisingContext extends ActionContextBase {
|
||||
createNationId: () => number;
|
||||
listNations?: () => Nation[];
|
||||
}
|
||||
|
||||
const ACTION_NAME = '거병';
|
||||
|
||||
export class ActionDefinition<
|
||||
@@ -19,7 +25,6 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(_raw: unknown): UprisingArgs | null {
|
||||
void _raw;
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -32,24 +37,92 @@ export class ActionDefinition<
|
||||
_args: UprisingArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
const uprisingCtx = context as unknown as UprisingContext;
|
||||
|
||||
// 직접 수정 (Immer Draft)
|
||||
general.meta = {
|
||||
...(general.meta as object),
|
||||
uprising: true as TriggerValue,
|
||||
if (!uprisingCtx.createNationId) {
|
||||
throw new Error('createNationId is not defined in context');
|
||||
}
|
||||
|
||||
const newNationId = uprisingCtx.createNationId();
|
||||
const josaYi = '이'; // Mock JodaUtil.pick
|
||||
|
||||
let nationName = general.name;
|
||||
const nations = uprisingCtx.listNations ? uprisingCtx.listNations() : [];
|
||||
|
||||
if (nations.some((n) => n.name === nationName)) {
|
||||
nationName = '㉥' + nationName;
|
||||
if (nationName.length > 18) nationName = nationName.substring(0, 18);
|
||||
}
|
||||
|
||||
if (nations.some((n) => n.name === nationName)) {
|
||||
nationName = '㉥' + nationName;
|
||||
}
|
||||
|
||||
const newNation: Nation = {
|
||||
id: newNationId,
|
||||
name: nationName,
|
||||
color: '#330000',
|
||||
typeCode: 'che_중립',
|
||||
level: 0,
|
||||
capitalCityId: null,
|
||||
chiefGeneralId: general.id,
|
||||
gold: 0,
|
||||
rice: 2000,
|
||||
power: 0,
|
||||
meta: {
|
||||
rate: 20,
|
||||
bill: 100,
|
||||
strategic_cmd_limit: 12,
|
||||
surlimit: 72,
|
||||
secretlimit: 3,
|
||||
gennum: 1,
|
||||
},
|
||||
};
|
||||
|
||||
context.addLog(`${ACTION_NAME}을 준비했습니다.`, {
|
||||
const cityName = context.city?.name ?? '??';
|
||||
|
||||
context.addLog(`거병에 성공하였습니다.`, {
|
||||
category: LogCategory.USER,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
context.addLog(`${general.name}${josaYi} ${cityName}에 거병하였습니다.`, {
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
context.addLog(`【거병】${general.name}${josaYi} 세력을 결성하였습니다.`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
context.addLog(`${cityName}에서 거병`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
context.addLog(`${general.name}${josaYi} ${cityName}에서 거병`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
|
||||
return { effects: [] };
|
||||
const effects = [
|
||||
createNationAddEffect(newNation),
|
||||
createGeneralPatchEffect<TriggerState>({
|
||||
nationId: newNationId,
|
||||
officerLevel: 12,
|
||||
experience: (general.experience || 0) + 100,
|
||||
dedication: (general.dedication || 0) + 100,
|
||||
}),
|
||||
];
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
||||
export const actionContextBuilder = defaultActionContextBuilder;
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
return {
|
||||
...base,
|
||||
createNationId: options.createNationId,
|
||||
listNations: () => options.worldRef?.listNations() ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: GeneralTurnCommandSpec = {
|
||||
key: 'che_거병',
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
beMonarch,
|
||||
beWanderingNation,
|
||||
reqNationGeneralCount,
|
||||
beOpeningPart,
|
||||
beNeutralCity,
|
||||
reqCityLevel,
|
||||
reqNationGeneralCount,
|
||||
checkNationNameDuplicate,
|
||||
beOpeningPart,
|
||||
} 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 {
|
||||
createCityPatchEffect,
|
||||
createGeneralPatchEffect,
|
||||
createNationPatchEffect,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat } 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';
|
||||
@@ -24,6 +29,42 @@ export interface FoundingArgs {
|
||||
|
||||
const ACTION_NAME = '건국';
|
||||
|
||||
const NATION_COLORS = [
|
||||
'#FF0000',
|
||||
'#800000',
|
||||
'#A0522D',
|
||||
'#FF6347',
|
||||
'#FFA500',
|
||||
'#FFDAB9',
|
||||
'#FFD700',
|
||||
'#FFFF00',
|
||||
'#7CFC00',
|
||||
'#00FF00',
|
||||
'#808000',
|
||||
'#008000',
|
||||
'#2E8B57',
|
||||
'#008080',
|
||||
'#20B2AA',
|
||||
'#6495ED',
|
||||
'#7FFFD4',
|
||||
'#AFEEEE',
|
||||
'#87CEEB',
|
||||
'#00FFFF',
|
||||
'#00BFFF',
|
||||
'#0000FF',
|
||||
'#000080',
|
||||
'#483D8B',
|
||||
'#7B68EE',
|
||||
'#BA55D3',
|
||||
'#800080',
|
||||
'#FF00FF',
|
||||
'#FFC0CB',
|
||||
'#F5F5DC',
|
||||
'#E0FFFF',
|
||||
'#FFFFFF',
|
||||
'#A9A9A9',
|
||||
];
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, FoundingArgs> {
|
||||
@@ -57,20 +98,67 @@ export class ActionDefinition<
|
||||
args: FoundingArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
const nation = context.nation!;
|
||||
const cityId = general.cityId!;
|
||||
|
||||
// 직접 수정 (Immer Draft)
|
||||
general.meta = {
|
||||
...(general.meta as object),
|
||||
founding: true as TriggerValue,
|
||||
foundingArgs: args as unknown as TriggerValue, // Cast to TriggerValue to solve type mismatch
|
||||
};
|
||||
if (args.colorType < 0 || args.colorType >= NATION_COLORS.length) {
|
||||
throw new Error('Invalid color type');
|
||||
}
|
||||
const color = NATION_COLORS[args.colorType];
|
||||
|
||||
context.addLog(`${args.nationName} 건국을 준비했습니다.`, {
|
||||
const josaUl = '을'; // Mock JosaUtil.pick
|
||||
const josaYi = '이';
|
||||
const city = context.city;
|
||||
|
||||
context.addLog(`${args.nationName}${josaUl} 건국하였습니다.`, {
|
||||
category: LogCategory.USER,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
context.addLog(`${general.name}${josaYi} ${city?.name}에 국가를 건설하였습니다.`, {
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
context.addLog(`【건국】${args.nationType} ${args.nationName}${josaYi} 새로이 등장하였습니다.`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
context.addLog(`${args.nationName}${josaUl} 건국`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
context.addLog(`${general.name}${josaYi} ${args.nationName}${josaUl} 건국`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
|
||||
return { effects: [] };
|
||||
const effects = [
|
||||
createNationPatchEffect(
|
||||
{
|
||||
name: args.nationName,
|
||||
typeCode: args.nationType,
|
||||
color: color!,
|
||||
level: 1, // Normal Nation
|
||||
capitalCityId: cityId,
|
||||
meta: {
|
||||
...nation.meta,
|
||||
can_국기변경: 1,
|
||||
},
|
||||
},
|
||||
nation.id
|
||||
),
|
||||
createCityPatchEffect(
|
||||
{
|
||||
nationId: nation.id,
|
||||
},
|
||||
cityId
|
||||
),
|
||||
createGeneralPatchEffect<TriggerState>({
|
||||
experience: (general.experience || 0) + 1000,
|
||||
dedication: (general.dedication || 0) + 1000,
|
||||
}),
|
||||
];
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,22 +21,19 @@ export const notOpeningPart = (relYear: number, openingPartYear: number): Constr
|
||||
export const beOpeningPart = (): Constraint => ({
|
||||
name: 'BeOpeningPart',
|
||||
requires: () => [
|
||||
{ kind: 'env', key: 'world' },
|
||||
{ kind: 'env', key: 'year' },
|
||||
{ kind: 'env', key: 'openingPartYear' },
|
||||
],
|
||||
test: (_ctx, view) => {
|
||||
const world = view.get({ kind: 'env', key: 'world' }) as { currentYear: number } | null;
|
||||
const openingPartYear = view.get({ kind: 'env', key: 'openingPartYear' }) as number | null;
|
||||
if (!world || openingPartYear === null) {
|
||||
return {
|
||||
kind: 'unknown',
|
||||
missing: [
|
||||
{ kind: 'env', key: 'world' },
|
||||
{ kind: 'env', key: 'openingPartYear' },
|
||||
],
|
||||
};
|
||||
const year = view.get({ kind: 'env', key: 'year' }) as number | undefined;
|
||||
const openingPartYear = view.get({ kind: 'env', key: 'openingPartYear' }) as number | undefined;
|
||||
|
||||
if (year === undefined || openingPartYear === undefined) {
|
||||
// 정보가 없으면 제약을 무시하거나 알림
|
||||
return allow();
|
||||
}
|
||||
if (world.currentYear < openingPartYear) {
|
||||
|
||||
if (year <= openingPartYear) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '초반이 지났습니다.' };
|
||||
|
||||
@@ -261,12 +261,12 @@ export const reqNationGeneralCount = (min: number): Constraint => ({
|
||||
export const checkNationNameDuplicate = (name: string): Constraint => ({
|
||||
name: 'CheckNationNameDuplicate',
|
||||
requires: () => [{ kind: 'nationList' }],
|
||||
test: (_ctx, view) => {
|
||||
test: (ctx, view) => {
|
||||
const nations = view.get({ kind: 'nationList' }) as Nation[] | null;
|
||||
if (!nations) {
|
||||
return { kind: 'unknown', missing: [{ kind: 'nationList' }] };
|
||||
}
|
||||
if (nations.some((n) => n.name === name)) {
|
||||
if (nations.some((n) => n.name === name && n.id !== ctx.nationId)) {
|
||||
return { kind: 'deny', reason: '이미 존재하는 국가 이름입니다.' };
|
||||
}
|
||||
return allow();
|
||||
|
||||
Reference in New Issue
Block a user