사령턴 추가 구현
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
import type { City, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
|
import { createLogEffect, createNationPatchEffect, createCityPatchEffect } 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 type { NationTurnCommandSpec } from './index.js';
|
||||||
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
|
|
||||||
|
export interface ReduceCityArgs { }
|
||||||
|
|
||||||
|
export interface ReduceCityResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
capitalCity: City;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '감축';
|
||||||
|
|
||||||
|
const POP_INCREASE = 100000;
|
||||||
|
const DEVEL_INCREASE = 2000;
|
||||||
|
const WALL_INCREASE = 2000;
|
||||||
|
const DEFAULT_COST = 60000;
|
||||||
|
const COST_COEF = 500;
|
||||||
|
const MIN_POP = 30000;
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, ReduceCityArgs, ReduceCityResolveContext<TriggerState>> {
|
||||||
|
public readonly key = 'che_감축';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
constructor(private readonly env: TurnCommandEnv) { }
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): ReduceCityArgs | null {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRecoverAmount(): number {
|
||||||
|
return this.env.develCost * COST_COEF + DEFAULT_COST / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: ReduceCityArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
{
|
||||||
|
name: 'reducibleCity',
|
||||||
|
requires: (ctx) => [
|
||||||
|
{ kind: 'nation', id: ctx.nationId! },
|
||||||
|
{ kind: 'city', id: 0 }
|
||||||
|
],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
const capitalCityId = nation?.capitalCityId;
|
||||||
|
|
||||||
|
if (!capitalCityId) return { kind: 'deny', reason: '방랑상태에서는 불가능합니다.' };
|
||||||
|
|
||||||
|
const capitalCity = view.get({ kind: 'city', id: capitalCityId }) as City | undefined;
|
||||||
|
if (!capitalCity) return { kind: 'deny', reason: '수도 정보를 찾을 수 없습니다.' };
|
||||||
|
if (capitalCity.level <= 4) return { kind: 'deny', reason: '더이상 감축할 수 없습니다.' };
|
||||||
|
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: ReduceCityResolveContext<TriggerState>,
|
||||||
|
_args: ReduceCityArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation, capitalCity } = context;
|
||||||
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoverAmount = this.getRecoverAmount();
|
||||||
|
const generalName = general.name;
|
||||||
|
const nationName = nation.name;
|
||||||
|
const destCityName = capitalCity.name;
|
||||||
|
|
||||||
|
const josaUl = JosaUtil.pick(destCityName, '을');
|
||||||
|
const josaYi = JosaUtil.pick(generalName, '이');
|
||||||
|
const josaYiNation = JosaUtil.pick(nationName, '이');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createCityPatchEffect({
|
||||||
|
level: capitalCity.level - 1,
|
||||||
|
population: Math.max(capitalCity.population - POP_INCREASE, MIN_POP),
|
||||||
|
agriculture: Math.max(capitalCity.agriculture - DEVEL_INCREASE, 0),
|
||||||
|
commerce: Math.max(capitalCity.commerce - DEVEL_INCREASE, 0),
|
||||||
|
security: Math.max(capitalCity.security - DEVEL_INCREASE, 0),
|
||||||
|
defence: Math.max(capitalCity.defence - WALL_INCREASE, 0),
|
||||||
|
wall: Math.max(capitalCity.wall - WALL_INCREASE, 0),
|
||||||
|
populationMax: capitalCity.populationMax - POP_INCREASE,
|
||||||
|
agricultureMax: capitalCity.agricultureMax - DEVEL_INCREASE,
|
||||||
|
commerceMax: capitalCity.commerceMax - DEVEL_INCREASE,
|
||||||
|
securityMax: capitalCity.securityMax - DEVEL_INCREASE,
|
||||||
|
defenceMax: capitalCity.defenceMax - WALL_INCREASE,
|
||||||
|
wallMax: capitalCity.wallMax - WALL_INCREASE,
|
||||||
|
}, capitalCity.id),
|
||||||
|
createNationPatchEffect({
|
||||||
|
gold: nation.gold + recoverAmount,
|
||||||
|
rice: nation.rice + recoverAmount,
|
||||||
|
}, nation.id),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<M><b>【${ACTION_NAME}】</b></><D><b>${nationName}</b></>${josaYiNation} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 30;
|
||||||
|
general.dedication += 30;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const nation = base.nation;
|
||||||
|
if (!nation || !nation.capitalCityId) return null;
|
||||||
|
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) return null;
|
||||||
|
|
||||||
|
const capitalCity = worldRef.getCityById(nation.capitalCityId);
|
||||||
|
if (!capitalCity) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
capitalCity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_감축',
|
||||||
|
category: '국가',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import type { StateView } from '@sammo-ts/logic/world/stateView.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, 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 { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
|
export interface ChangeFlagArgs {
|
||||||
|
colorType: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ChangeFlagArgs> {
|
||||||
|
public readonly key = 'che_국기변경';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): ChangeFlagArgs | null {
|
||||||
|
const data = raw as { colorType?: number };
|
||||||
|
if (typeof data?.colorType !== 'number') return null;
|
||||||
|
if (data.colorType < 0 || data.colorType >= NATION_COLORS.length) return null;
|
||||||
|
return { colorType: data.colorType };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: ChangeFlagArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
{
|
||||||
|
name: 'canChangeFlag',
|
||||||
|
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
const canChange = ((nation?.meta as any)?.[`can_${ACTION_NAME}`] ?? 0) !== 0;
|
||||||
|
if (!canChange) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
args: ChangeFlagArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation } = context;
|
||||||
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = NATION_COLORS[args.colorType];
|
||||||
|
const generalName = general.name;
|
||||||
|
const nationName = nation.name;
|
||||||
|
|
||||||
|
const josaYi = JosaUtil.pick(generalName, '이');
|
||||||
|
const josaYiNation = JosaUtil.pick(nationName, '이');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createNationPatchEffect({
|
||||||
|
color: color!,
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
[`can_${ACTION_NAME}`]: 0,
|
||||||
|
},
|
||||||
|
}, nation.id),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<S><b>【${ACTION_NAME}】</b></><D><b>${nationName}</b></>${josaYiNation} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다.`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(`<span style='color:${color};'><b>국기</b></span>를 변경하였습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 5;
|
||||||
|
general.dedication += 5;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_국기변경',
|
||||||
|
category: '국가',
|
||||||
|
reqArg: true,
|
||||||
|
args: { colorType: 0 },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
checkNationNameDuplicate,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, 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 { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
|
export interface ChangeNationNameArgs {
|
||||||
|
nationName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '국호변경';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, ChangeNationNameArgs> {
|
||||||
|
public readonly key = 'che_국호변경';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): ChangeNationNameArgs | null {
|
||||||
|
const data = raw as { nationName?: string };
|
||||||
|
if (typeof data?.nationName !== 'string') return null;
|
||||||
|
|
||||||
|
const name = data.nationName.trim();
|
||||||
|
if (name.length < 1 || name.length > 8) return null;
|
||||||
|
|
||||||
|
return { nationName: name };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, args: ChangeNationNameArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
checkNationNameDuplicate(args.nationName),
|
||||||
|
{
|
||||||
|
name: 'canChangeNationName',
|
||||||
|
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||||
|
test: (_ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
const canChange = ((nation?.meta as any)?.[`can_${ACTION_NAME}`] ?? 0) !== 0;
|
||||||
|
if (!canChange) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
args: ChangeNationNameArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation } = context;
|
||||||
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalName = general.name;
|
||||||
|
const oldNationName = nation.name;
|
||||||
|
const newNationName = args.nationName;
|
||||||
|
|
||||||
|
const josaYi = JosaUtil.pick(generalName, '이');
|
||||||
|
const josaYiNation = JosaUtil.pick(newNationName, '이');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createNationPatchEffect({
|
||||||
|
name: newNationName,
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
[`can_${ACTION_NAME}`]: 0,
|
||||||
|
},
|
||||||
|
}, nation.id),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} 국호를 <D><b>${newNationName}</b></>로 변경하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<S><b>【${ACTION_NAME}】</b></><D><b>${oldNationName}</b></>${josaYiNation} 국호를 <D><b>${newNationName}</b></>로 변경하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} 국호를 <D><b>${newNationName}</b></>로 변경하였습니다.`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(`국호를 <D><b>${newNationName}</b></>로 변경하였습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 5;
|
||||||
|
general.dedication += 5;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_국호변경',
|
||||||
|
category: '국가',
|
||||||
|
reqArg: true,
|
||||||
|
args: { nationName: '' },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
existsDestGeneral,
|
||||||
|
friendlyDestGeneral,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
|
import { createLogEffect, createNationPatchEffect, createGeneralPatchEffect } 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 type { NationTurnCommandSpec } from './index.js';
|
||||||
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
|
import { clamp } from 'es-toolkit';
|
||||||
|
|
||||||
|
export interface SeizureArgs {
|
||||||
|
isGold: boolean;
|
||||||
|
amount: number;
|
||||||
|
destGeneralID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeizureResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destGeneral: General<TriggerState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '몰수';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, SeizureArgs, SeizureResolveContext<TriggerState>> {
|
||||||
|
public readonly key = 'che_몰수';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
constructor(private readonly env: TurnCommandEnv) { }
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): SeizureArgs | null {
|
||||||
|
const data = raw as { isGold?: boolean; amount?: number; destGeneralID?: number };
|
||||||
|
if (typeof data?.isGold !== 'boolean') return null;
|
||||||
|
if (typeof data?.amount !== 'number') return null;
|
||||||
|
if (typeof data?.destGeneralID !== 'number') return null;
|
||||||
|
|
||||||
|
const amount = Math.floor(data.amount / 100) * 100;
|
||||||
|
if (amount <= 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGold: data.isGold,
|
||||||
|
amount: clamp(amount, 100, this.env.maxResourceActionAmount ?? 10000),
|
||||||
|
destGeneralID: data.destGeneralID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, args: SeizureArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
existsDestGeneral(),
|
||||||
|
friendlyDestGeneral(),
|
||||||
|
{
|
||||||
|
name: 'notSelf',
|
||||||
|
requires: () => [],
|
||||||
|
test: (ctx: ConstraintContext) => {
|
||||||
|
if (ctx.actorId === args.destGeneralID) {
|
||||||
|
return { kind: 'deny', reason: '본인입니다' };
|
||||||
|
}
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: SeizureResolveContext<TriggerState>,
|
||||||
|
args: SeizureArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation, destGeneral } = context;
|
||||||
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resKey = args.isGold ? 'gold' : 'rice';
|
||||||
|
const resName = args.isGold ? '금' : '쌀';
|
||||||
|
|
||||||
|
const actualAmount = clamp(args.amount, 0, (destGeneral as any)[resKey] ?? 0);
|
||||||
|
|
||||||
|
if (actualAmount <= 0) {
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect(`${destGeneral.name}에게서 몰수할 ${resName}이 없습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountText = actualAmount.toLocaleString();
|
||||||
|
const josaUl = JosaUtil.pick(amountText, '을');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createGeneralPatchEffect({
|
||||||
|
[resKey]: (destGeneral as any)[resKey] - actualAmount,
|
||||||
|
}, destGeneral.id),
|
||||||
|
createNationPatchEffect({
|
||||||
|
[resKey]: (nation as any)[resKey] + actualAmount,
|
||||||
|
}, nation.id),
|
||||||
|
// Actor General Action Log
|
||||||
|
createLogEffect(`<Y>${destGeneral.name}</>에게서 ${resName} <C>${amountText}</>${josaUl} 몰수했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
// Target General Action Log
|
||||||
|
createLogEffect(`${resName} ${amountText}${josaUl} 몰수 당했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
generalId: destGeneral.id,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 5;
|
||||||
|
general.dedication += 5;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const destGeneralId = options.actionArgs.destGeneralID;
|
||||||
|
if (typeof destGeneralId !== 'number') return null;
|
||||||
|
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) return null;
|
||||||
|
|
||||||
|
const destGeneral = worldRef.getGeneralById(destGeneralId);
|
||||||
|
if (!destGeneral) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destGeneral,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_몰수',
|
||||||
|
category: '인사',
|
||||||
|
reqArg: true,
|
||||||
|
args: { isGold: false, amount: 0, destGeneralID: 0 },
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||||
|
};
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import type { GeneralTriggerState, Nation, City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beMonarch,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
|
import { createLogEffect, createNationPatchEffect, createCityPatchEffect, createGeneralPatchEffect } 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 type { NationTurnCommandSpec } from './index.js';
|
||||||
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
|
|
||||||
|
export interface RandomMoveCapitalArgs { }
|
||||||
|
|
||||||
|
export interface RandomMoveCapitalResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
neutralCandidateCities: City[];
|
||||||
|
nationGenerals: General<TriggerState>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '무작위 수도 이전';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, RandomMoveCapitalArgs, RandomMoveCapitalResolveContext<TriggerState>> {
|
||||||
|
public readonly key = 'che_무작위수도이전';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): RandomMoveCapitalArgs | null {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: RandomMoveCapitalArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beMonarch(),
|
||||||
|
suppliedCity(),
|
||||||
|
{
|
||||||
|
name: 'canRandomMoveCapital',
|
||||||
|
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
const canMove = ((nation?.meta as any)?.[`can_무작위수도이전`] ?? 0) > 0;
|
||||||
|
if (!canMove) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: RandomMoveCapitalResolveContext<TriggerState>,
|
||||||
|
_args: RandomMoveCapitalArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation, neutralCandidateCities, nationGenerals, rng } = context;
|
||||||
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neutralCandidateCities.length === 0) {
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect(`이동할 수 있는 도시가 없습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const destCity = neutralCandidateCities[rng.nextInt(0, neutralCandidateCities.length)]!;
|
||||||
|
const oldCityId = nation.capitalCityId;
|
||||||
|
const generalName = general.name;
|
||||||
|
const nationName = nation.name;
|
||||||
|
const destCityName = destCity.name;
|
||||||
|
|
||||||
|
const josaRo = JosaUtil.pick(destCityName, '로');
|
||||||
|
const josaYi = JosaUtil.pick(generalName, '이');
|
||||||
|
const josaYiNation = JosaUtil.pick(nationName, '이');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createNationPatchEffect({
|
||||||
|
capitalCityId: destCity.id,
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
can_무작위수도이전: ((nation.meta as any).can_무작위수도이전 ?? 0) - 1,
|
||||||
|
},
|
||||||
|
}, nation.id),
|
||||||
|
// Set new capital
|
||||||
|
createCityPatchEffect({
|
||||||
|
nationId: nation.id,
|
||||||
|
}, destCity.id),
|
||||||
|
// Set old capital to neutral
|
||||||
|
createCityPatchEffect({
|
||||||
|
nationId: 0,
|
||||||
|
frontState: 0,
|
||||||
|
}, oldCityId!),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaRo} <M>수도 이전</}하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<S><b>【${ACTION_NAME}】</b></><D><b>${nationName}</b></>${josaYiNation} <G><b>${destCityName}</b></>${josaRo} <M>수도 이전</}하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</} `, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} 국가를 옮겼습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Move all generals to new capital
|
||||||
|
for (const targetGeneral of nationGenerals) {
|
||||||
|
effects.push(createGeneralPatchEffect({
|
||||||
|
cityId: destCity.id,
|
||||||
|
}, targetGeneral.id));
|
||||||
|
|
||||||
|
if (targetGeneral.id !== general.id) {
|
||||||
|
effects.push(createLogEffect(`국가 수도를 <G><b>${destCityName}</b></>${josaRo} 옮겼습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
generalId: targetGeneral.id,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
general.experience += 10;
|
||||||
|
general.dedication += 10;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) return null;
|
||||||
|
|
||||||
|
const allCities = worldRef.listCities();
|
||||||
|
const neutralCandidateCities = allCities.filter(c => c.nationId === 0 && c.level >= 5 && c.level <= 6);
|
||||||
|
const nationGenerals = worldRef.listGenerals().filter(g => g.nationId === base.general.nationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
neutralCandidateCities,
|
||||||
|
nationGenerals,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_무작위수도이전',
|
||||||
|
category: '국가',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
differentDestNation,
|
||||||
|
existsDestNation,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, 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 { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
|
import { clamp } from 'es-toolkit';
|
||||||
|
|
||||||
|
export interface MaterialAidArgs {
|
||||||
|
destNationId: number;
|
||||||
|
amountList: [number, number]; // [gold, rice]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterialAidResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destNation: Nation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '원조';
|
||||||
|
const COEF_AID_AMOUNT = 10000;
|
||||||
|
const POST_REQ_TURN = 12;
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, MaterialAidArgs, MaterialAidResolveContext<TriggerState>> {
|
||||||
|
public readonly key = 'che_물자원조';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
constructor(private readonly env: TurnCommandEnv) { }
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): MaterialAidArgs | null {
|
||||||
|
const data = raw as { destNationId?: number; amountList?: [number, number] };
|
||||||
|
if (typeof data?.destNationId !== 'number') return null;
|
||||||
|
if (!Array.isArray(data.amountList) || data.amountList.length !== 2) return null;
|
||||||
|
if (typeof data.amountList[0] !== 'number' || typeof data.amountList[1] !== 'number') return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
destNationId: data.destNationId,
|
||||||
|
amountList: [data.amountList[0], data.amountList[1]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] {
|
||||||
|
const [goldAmount, riceAmount] = args.amountList;
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
existsDestNation(),
|
||||||
|
differentDestNation(),
|
||||||
|
{
|
||||||
|
name: 'aidLimit',
|
||||||
|
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
const limit = (nation?.level ?? 1) * COEF_AID_AMOUNT;
|
||||||
|
if (goldAmount > limit || riceAmount > limit) {
|
||||||
|
return { kind: 'deny', reason: '작위 제한량 이상은 보낼 수 없습니다.' };
|
||||||
|
}
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nationSurlimit',
|
||||||
|
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||||
|
test: (_ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: _ctx.nationId! }) as Nation | undefined;
|
||||||
|
const surlimit = (nation?.meta as any)?.surlimit ?? 0;
|
||||||
|
if (surlimit > 0) return { kind: 'deny', reason: '외교제한중입니다.' };
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'destNationSurlimit',
|
||||||
|
requires: () => [{ kind: 'nation', id: args.destNationId }],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const destNation = view.get({ kind: 'nation', id: args.destNationId }) as Nation | undefined;
|
||||||
|
const surlimit = (destNation?.meta as any)?.surlimit ?? 0;
|
||||||
|
if (surlimit > 0) return { kind: 'deny', reason: '상대국이 외교제한중입니다.' };
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: MaterialAidResolveContext<TriggerState>,
|
||||||
|
args: MaterialAidArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation, destNation } = context;
|
||||||
|
if (!nation || !destNation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [goldAmount, riceAmount] = args.amountList;
|
||||||
|
|
||||||
|
// Fit amount to nation's resources
|
||||||
|
const actualGold = clamp(goldAmount, 0, Math.max(0, nation.gold - this.env.baseGold));
|
||||||
|
const actualRice = clamp(riceAmount, 0, Math.max(0, nation.rice - this.env.baseRice));
|
||||||
|
|
||||||
|
const goldText = actualGold.toLocaleString();
|
||||||
|
const riceText = actualRice.toLocaleString();
|
||||||
|
const josaUlRice = JosaUtil.pick(riceText, '을');
|
||||||
|
const josaRo = JosaUtil.pick(destNation.name, '로');
|
||||||
|
const nationName = nation.name;
|
||||||
|
|
||||||
|
const broadcastMessage = `<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>을 지원했습니다.`;
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createNationPatchEffect({
|
||||||
|
gold: nation.gold - actualGold,
|
||||||
|
rice: nation.rice - actualRice,
|
||||||
|
meta: {
|
||||||
|
...nation.meta,
|
||||||
|
surlimit: ((nation.meta as any)?.surlimit ?? 0) + POST_REQ_TURN,
|
||||||
|
},
|
||||||
|
}, nation.id),
|
||||||
|
createNationPatchEffect({
|
||||||
|
gold: destNation.gold + actualGold,
|
||||||
|
rice: destNation.rice + actualRice,
|
||||||
|
}, destNation.id),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<Y><b>【${ACTION_NAME}】</b></><D><b>${nationName}</b></>에서 <D><b>${destNation.name}</b></>${josaRo} 물자를 지원합니다`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<D><b>${destNation.name}</b></>${josaRo} 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Dest Nation History Log
|
||||||
|
createLogEffect(`<D><b>${nationName}</b></>로부터 금<C>${goldText}</> 쌀<C>${riceText}</>${josaUlRice} 지원 받음`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: destNation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(broadcastMessage, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 5;
|
||||||
|
general.dedication += 5;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const destNationId = options.actionArgs.destNationId;
|
||||||
|
if (typeof destNationId !== 'number') return null;
|
||||||
|
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) return null;
|
||||||
|
|
||||||
|
const destNation = worldRef.getNationById(destNationId);
|
||||||
|
if (!destNation) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destNation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_물자원조',
|
||||||
|
category: '외교',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destNationId: 0, amountList: [0, 0] },
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||||
|
};
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
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 type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
beChief,
|
beChief,
|
||||||
disallowDiplomacyBetweenStatus,
|
disallowDiplomacyBetweenStatus,
|
||||||
existsDestNation,
|
existsDestNation,
|
||||||
|
nearNation,
|
||||||
notBeNeutral,
|
notBeNeutral,
|
||||||
occupiedCity,
|
occupiedCity,
|
||||||
suppliedCity,
|
suppliedCity,
|
||||||
} from '@sammo-ts/logic/constraints/presets.js';
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
import type {
|
||||||
|
GeneralActionEffect,
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.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';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
import type { NationTurnCommandSpec } from './index.js';
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface DeclareWarArgs {
|
export interface DeclareWarArgs {
|
||||||
destNationId: number;
|
destNationId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeclareWarResolveContext is not used anymore as it was replaced by inline type in ActionDefinition
|
||||||
|
|
||||||
const ACTION_NAME = '선전포고';
|
const ACTION_NAME = '선전포고';
|
||||||
// legacy 규칙: 선전포고 상태는 24턴 유지.
|
// legacy 규칙: 선전포고 상태는 24턴 유지.
|
||||||
const DIPLOMACY_DECLARE = 1;
|
const DIPLOMACY_DECLARE = 1;
|
||||||
@@ -34,7 +42,11 @@ const parseNationId = (raw: unknown): number | null => {
|
|||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionDefinition<TriggerState, DeclareWarArgs> {
|
> implements GeneralActionDefinition<
|
||||||
|
TriggerState,
|
||||||
|
DeclareWarArgs,
|
||||||
|
GeneralActionResolveContext<TriggerState> & { destNation: Nation }
|
||||||
|
> {
|
||||||
public readonly key = 'che_선전포고';
|
public readonly key = 'che_선전포고';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
@@ -48,12 +60,24 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildConstraints(_ctx: ConstraintContext, _args: DeclareWarArgs): Constraint[] {
|
buildConstraints(_ctx: ConstraintContext, _args: DeclareWarArgs): Constraint[] {
|
||||||
|
const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
notBeNeutral(),
|
notBeNeutral(),
|
||||||
occupiedCity(),
|
occupiedCity(),
|
||||||
suppliedCity(),
|
suppliedCity(),
|
||||||
beChief(),
|
beChief(),
|
||||||
existsDestNation(),
|
existsDestNation(),
|
||||||
|
nearNation(),
|
||||||
|
// legacy: startYear + 1. 0-indexed relYear로는 1
|
||||||
|
{
|
||||||
|
name: 'declareWarYearLimit',
|
||||||
|
requires: () => [],
|
||||||
|
test: () => {
|
||||||
|
if (relYear >= 1) return { kind: 'allow' };
|
||||||
|
return { kind: 'deny', reason: '초반제한 해제 2년전부터 가능합니다.' };
|
||||||
|
},
|
||||||
|
},
|
||||||
disallowDiplomacyBetweenStatus({
|
disallowDiplomacyBetweenStatus({
|
||||||
0: '아국과 이미 교전중입니다.',
|
0: '아국과 이미 교전중입니다.',
|
||||||
1: '아국과 이미 선포중입니다.',
|
1: '아국과 이미 선포중입니다.',
|
||||||
@@ -63,14 +87,14 @@ export class ActionDefinition<
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: GeneralActionResolveContext<TriggerState>,
|
context: GeneralActionResolveContext<TriggerState> & { destNation: Nation },
|
||||||
args: DeclareWarArgs
|
args: DeclareWarArgs
|
||||||
): GeneralActionOutcome<TriggerState> {
|
): GeneralActionOutcome<TriggerState> {
|
||||||
const nationId = context.nation?.id;
|
const nationId = context.nation?.id;
|
||||||
if (nationId === undefined || nationId <= 0) {
|
if (nationId === undefined || nationId <= 0 || !context.destNation) {
|
||||||
return {
|
return {
|
||||||
effects: [
|
effects: [
|
||||||
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
|
createLogEffect(`${ACTION_NAME}을 준비했지만 국가(또는 대상 국가) 정보가 없습니다.`, {
|
||||||
scope: LogScope.GENERAL,
|
scope: LogScope.GENERAL,
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.ACTION,
|
||||||
format: LogFormat.MONTH,
|
format: LogFormat.MONTH,
|
||||||
@@ -78,28 +102,86 @@ export class ActionDefinition<
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
effects: [
|
const nationName = context.nation?.name ?? '아국';
|
||||||
createDiplomacyPatchEffect(nationId, args.destNationId, {
|
const destNationName = context.destNation.name;
|
||||||
state: DIPLOMACY_DECLARE,
|
const generalName = context.general.name;
|
||||||
term: DECLARE_TERM,
|
const josaGa = JosaUtil.pick(nationName, '가');
|
||||||
}),
|
const josaGaGeneral = JosaUtil.pick(generalName, '이');
|
||||||
createDiplomacyPatchEffect(args.destNationId, nationId, {
|
|
||||||
state: DIPLOMACY_DECLARE,
|
const broadcastMessage = `<Y>${nationName}</>${josaGa} <Y>${destNationName}</>에게 <R>${ACTION_NAME}</>하였습니다!`;
|
||||||
term: DECLARE_TERM,
|
const historyMessage = `<Y>${nationName}</>${josaGa} <Y>${destNationName}</>에게 <R>${ACTION_NAME}</>`;
|
||||||
}),
|
|
||||||
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
scope: LogScope.GENERAL,
|
createDiplomacyPatchEffect(nationId, args.destNationId, {
|
||||||
|
state: DIPLOMACY_DECLARE,
|
||||||
|
term: DECLARE_TERM,
|
||||||
|
}),
|
||||||
|
createDiplomacyPatchEffect(args.destNationId, nationId, {
|
||||||
|
state: DIPLOMACY_DECLARE,
|
||||||
|
term: DECLARE_TERM,
|
||||||
|
}),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(broadcastMessage, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(historyMessage, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(historyMessage, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nationId,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Target Nation History Log
|
||||||
|
createLogEffect(historyMessage, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: args.destNationId,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// National Message (국메)
|
||||||
|
createLogEffect(
|
||||||
|
`【국메】<Y>${generalName}</>${josaGaGeneral} <Y>${destNationName}</>에게 <R>${ACTION_NAME}</>하였습니다!`,
|
||||||
|
{
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nationId,
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.ACTION,
|
||||||
format: LogFormat.MONTH,
|
format: LogFormat.PLAIN,
|
||||||
}),
|
}
|
||||||
],
|
),
|
||||||
};
|
];
|
||||||
|
|
||||||
|
return { effects };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
|
// 예약 턴 실행에 필요한 대상 국가 정보를 구성한다.
|
||||||
export const actionContextBuilder = defaultActionContextBuilder;
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const destNationId = options.actionArgs.destNationId;
|
||||||
|
if (typeof destNationId !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const destNation = worldRef.getNationById(destNationId);
|
||||||
|
if (!destNation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destNation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const commandSpec: NationTurnCommandSpec = {
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
key: 'che_선전포고',
|
key: 'che_선전포고',
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type { City, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
reqNationGold,
|
||||||
|
reqNationRice,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
|
import { createLogEffect, createNationPatchEffect, createCityPatchEffect } 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 type { NationTurnCommandSpec } from './index.js';
|
||||||
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
|
|
||||||
|
export interface ExpandCityArgs { }
|
||||||
|
|
||||||
|
export interface ExpandCityResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
capitalCity: City;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '증축';
|
||||||
|
|
||||||
|
const POP_INCREASE = 100000;
|
||||||
|
const DEVEL_INCREASE = 2000;
|
||||||
|
const WALL_INCREASE = 2000;
|
||||||
|
const DEFAULT_COST = 60000;
|
||||||
|
const COST_COEF = 500;
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, ExpandCityArgs, ExpandCityResolveContext<TriggerState>> {
|
||||||
|
public readonly key = 'che_증축';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
constructor(private readonly env: TurnCommandEnv) { }
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): ExpandCityArgs | null {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCost(): number {
|
||||||
|
return this.env.develCost * COST_COEF + DEFAULT_COST;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: ExpandCityArgs): Constraint[] {
|
||||||
|
const cost = this.getCost();
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
reqNationGold(() => this.env.baseGold + cost),
|
||||||
|
reqNationRice(() => this.env.baseRice + cost),
|
||||||
|
{
|
||||||
|
name: 'expandableCity',
|
||||||
|
requires: (ctx) => [
|
||||||
|
{ kind: 'nation', id: ctx.nationId! },
|
||||||
|
{ kind: 'city', id: 0 }
|
||||||
|
],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
const capitalCityId = nation?.capitalCityId;
|
||||||
|
|
||||||
|
if (!capitalCityId) return { kind: 'deny', reason: '방랑상태에서는 불가능합니다.' };
|
||||||
|
|
||||||
|
const capitalCity = view.get({ kind: 'city', id: capitalCityId }) as City | undefined;
|
||||||
|
if (!capitalCity) return { kind: 'deny', reason: '수도 정보를 찾을 수 없습니다.' };
|
||||||
|
if (capitalCity.level <= 3) return { kind: 'deny', reason: '수진, 진, 관문에서는 불가능합니다.' };
|
||||||
|
if (capitalCity.level >= 8) return { kind: 'deny', reason: '더이상 증축할 수 없습니다.' };
|
||||||
|
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: ExpandCityResolveContext<TriggerState>,
|
||||||
|
_args: ExpandCityArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation, capitalCity } = context;
|
||||||
|
if (!nation) {
|
||||||
|
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cost = this.getCost();
|
||||||
|
const generalName = general.name;
|
||||||
|
const nationName = nation.name;
|
||||||
|
const destCityName = capitalCity.name;
|
||||||
|
|
||||||
|
const josaUl = JosaUtil.pick(destCityName, '을');
|
||||||
|
const josaYi = JosaUtil.pick(generalName, '이');
|
||||||
|
const josaYiNation = JosaUtil.pick(nationName, '이');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createCityPatchEffect({
|
||||||
|
level: capitalCity.level + 1,
|
||||||
|
populationMax: capitalCity.populationMax + POP_INCREASE,
|
||||||
|
agricultureMax: capitalCity.agricultureMax + DEVEL_INCREASE,
|
||||||
|
commerceMax: capitalCity.commerceMax + DEVEL_INCREASE,
|
||||||
|
securityMax: capitalCity.securityMax + DEVEL_INCREASE,
|
||||||
|
defenceMax: capitalCity.defenceMax + WALL_INCREASE,
|
||||||
|
wallMax: capitalCity.wallMax + WALL_INCREASE,
|
||||||
|
}, capitalCity.id),
|
||||||
|
createNationPatchEffect({
|
||||||
|
gold: nation.gold - cost,
|
||||||
|
rice: nation.rice - cost,
|
||||||
|
}, nation.id),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<C><b>【${ACTION_NAME}】</b></><D><b>${nationName}</b></>${josaYiNation} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaUl} <M>${ACTION_NAME}</>`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaUl} ${ACTION_NAME}했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 30;
|
||||||
|
general.dedication += 30;
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const nation = base.nation;
|
||||||
|
if (!nation || !nation.capitalCityId) return null;
|
||||||
|
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) return null;
|
||||||
|
|
||||||
|
const capitalCity = worldRef.getCityById(nation.capitalCityId);
|
||||||
|
if (!capitalCity) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
capitalCity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_증축',
|
||||||
|
category: '국가',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||||
|
};
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import type { City, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
occupiedCity,
|
||||||
|
occupiedDestCity,
|
||||||
|
suppliedCity,
|
||||||
|
suppliedDestCity,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type { GeneralActionEffect, 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';
|
||||||
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||||
|
|
||||||
|
export interface MoveCapitalArgs {
|
||||||
|
destCityID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoveCapitalResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destCity: City;
|
||||||
|
map: MapDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '천도';
|
||||||
|
|
||||||
|
const calcDistance = (
|
||||||
|
fromCityId: number,
|
||||||
|
toCityId: number,
|
||||||
|
map: MapDefinition
|
||||||
|
): number => {
|
||||||
|
if (fromCityId === toCityId) return 0;
|
||||||
|
|
||||||
|
const connections = new Map<number, number[]>();
|
||||||
|
|
||||||
|
for (const city of map.cities) {
|
||||||
|
connections.set(city.id, city.connections ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue: Array<[number, number]> = [[fromCityId, 0]];
|
||||||
|
const visited = new Set<number>([fromCityId]);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const [current, dist] = queue.shift()!;
|
||||||
|
if (current === toCityId) return dist;
|
||||||
|
|
||||||
|
const nextNodes = connections.get(current) ?? [];
|
||||||
|
for (const next of nextNodes) {
|
||||||
|
if (!visited.has(next)) {
|
||||||
|
visited.add(next);
|
||||||
|
queue.push([next, dist + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 50;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
> implements GeneralActionDefinition<TriggerState, MoveCapitalArgs, MoveCapitalResolveContext<TriggerState>> {
|
||||||
|
public readonly key = 'che_천도';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
constructor(private readonly env: TurnCommandEnv) { }
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): MoveCapitalArgs | null {
|
||||||
|
const data = raw as { destCityID?: number };
|
||||||
|
if (typeof data?.destCityID !== 'number') return null;
|
||||||
|
return { destCityID: data.destCityID };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, args: MoveCapitalArgs): Constraint[] {
|
||||||
|
const develcost = this.env.develCost;
|
||||||
|
|
||||||
|
return [
|
||||||
|
occupiedCity(),
|
||||||
|
occupiedDestCity(),
|
||||||
|
beChief(),
|
||||||
|
suppliedCity(),
|
||||||
|
suppliedDestCity(),
|
||||||
|
{
|
||||||
|
name: 'notCurrentCapital',
|
||||||
|
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||||
|
test: (ctx: ConstraintContext, view: StateView) => {
|
||||||
|
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||||
|
if (nation?.capitalCityId === args.destCityID) {
|
||||||
|
return { kind: 'deny', reason: '이미 수도입니다.' };
|
||||||
|
}
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reqMoveCapitalCost',
|
||||||
|
requires: (ctx) => [
|
||||||
|
{ kind: 'nation', id: ctx.nationId! },
|
||||||
|
{ kind: 'env', key: 'map' }
|
||||||
|
],
|
||||||
|
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;
|
||||||
|
if (!map || !nation || nation.capitalCityId === undefined || nation.capitalCityId === null) return { kind: 'allow' };
|
||||||
|
|
||||||
|
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
||||||
|
const cost = develcost * 5 * Math.pow(2, dist);
|
||||||
|
|
||||||
|
if (nation.gold < cost + 1000) return { kind: 'deny', reason: `금이 부족합니다. (필요: ${cost + 1000})` };
|
||||||
|
if (nation.rice < cost + 1000) return { kind: 'deny', reason: `쌀이 부족합니다. (필요: ${cost + 1000})` };
|
||||||
|
|
||||||
|
return { kind: 'allow' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: MoveCapitalResolveContext<TriggerState>,
|
||||||
|
args: MoveCapitalArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const { general, nation, destCity, map } = context;
|
||||||
|
if (!nation || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||||
|
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 generalName = general.name;
|
||||||
|
const nationName = nation.name;
|
||||||
|
const destCityName = destCity.name;
|
||||||
|
|
||||||
|
const josaRo = JosaUtil.pick(destCityName, '로');
|
||||||
|
const josaYi = JosaUtil.pick(generalName, '이');
|
||||||
|
const josaYiNation = JosaUtil.pick(nationName, '이');
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
|
createNationPatchEffect({
|
||||||
|
capitalCityId: args.destCityID,
|
||||||
|
gold: nation.gold - cost,
|
||||||
|
rice: nation.rice - cost,
|
||||||
|
}, nation.id),
|
||||||
|
// Global Action Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>를 명령하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
}),
|
||||||
|
// Global History Log
|
||||||
|
createLogEffect(`<S><b>【${ACTION_NAME}】</b></><D><b>${nationName}</b></>${josaYiNation} <G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</>하였습니다.`, {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// Actor Nation History Log
|
||||||
|
createLogEffect(`<Y>${generalName}</>${josaYi} <G><b>${destCityName}</b></>${josaRo} <M>${ACTION_NAME}</> 명령`, {
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
nationId: nation.id,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
}),
|
||||||
|
// General Action Log
|
||||||
|
createLogEffect(`<G><b>${destCityName}</b></>${josaRo} ${ACTION_NAME}했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
general.experience += 5 * (dist * 2 + 1);
|
||||||
|
general.dedication += 5 * (dist * 2 + 1);
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const destCityId = options.actionArgs.destCityID;
|
||||||
|
if (typeof destCityId !== 'number') return null;
|
||||||
|
|
||||||
|
const worldRef = options.worldRef;
|
||||||
|
if (!worldRef) return null;
|
||||||
|
|
||||||
|
const destCity = worldRef.getCityById(destCityId);
|
||||||
|
const map = options.map;
|
||||||
|
if (!destCity || !map) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destCity,
|
||||||
|
map,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_천도',
|
||||||
|
category: '국가',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destCityID: 0 },
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||||
|
};
|
||||||
@@ -15,6 +15,14 @@ export const NATION_TURN_COMMAND_KEYS = [
|
|||||||
'che_이호경식',
|
'che_이호경식',
|
||||||
'che_수몰',
|
'che_수몰',
|
||||||
'che_급습',
|
'che_급습',
|
||||||
|
'che_천도',
|
||||||
|
'che_국호변경',
|
||||||
|
'che_무작위수도이전',
|
||||||
|
'che_국기변경',
|
||||||
|
'che_증축',
|
||||||
|
'che_감축',
|
||||||
|
'che_몰수',
|
||||||
|
'che_물자원조',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type NationTurnCommandKey = (typeof NATION_TURN_COMMAND_KEYS)[number];
|
export type NationTurnCommandKey = (typeof NATION_TURN_COMMAND_KEYS)[number];
|
||||||
@@ -40,6 +48,14 @@ const defaultImporters: Record<NationTurnCommandKey, NationTurnCommandImporter>
|
|||||||
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'),
|
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'),
|
||||||
|
che_증축: async () => import('./che_증축.js'),
|
||||||
|
che_감축: async () => import('./che_감축.js'),
|
||||||
|
che_몰수: async () => import('./che_몰수.js'),
|
||||||
|
che_물자원조: async () => import('./che_물자원조.js'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNationTurnCommandKey = (value: string): value is NationTurnCommandKey =>
|
export const isNationTurnCommandKey = (value: string): value is NationTurnCommandKey =>
|
||||||
@@ -50,7 +66,7 @@ export class NationTurnCommandLoader {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly importers: Record<NationTurnCommandKey, NationTurnCommandImporter> = defaultImporters
|
private readonly importers: Record<NationTurnCommandKey, NationTurnCommandImporter> = defaultImporters
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async load(key: NationTurnCommandKey): Promise<NationTurnCommandModule> {
|
async load(key: NationTurnCommandKey): Promise<NationTurnCommandModule> {
|
||||||
const cached = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import { allow, readGeneral, readMetaNumber, readNation, resolveDestNationId, unknownOrDeny } from './helpers.js';
|
import { allow, readGeneral, readMetaNumber, readNation, resolveDestNationId, unknownOrDeny } from './helpers.js';
|
||||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
|
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
|
||||||
|
|
||||||
@@ -272,3 +272,68 @@ export const checkNationNameDuplicate = (name: string): Constraint => ({
|
|||||||
return allow();
|
return allow();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const nearNation = (): Constraint => ({
|
||||||
|
name: 'nearNation',
|
||||||
|
requires: (ctx) => {
|
||||||
|
const reqs: RequirementKey[] = [
|
||||||
|
{ kind: 'env', key: 'cities' },
|
||||||
|
{ kind: 'env', key: 'map' },
|
||||||
|
];
|
||||||
|
if (ctx.nationId !== undefined) {
|
||||||
|
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||||
|
} else {
|
||||||
|
reqs.push({ kind: 'general', id: ctx.actorId });
|
||||||
|
}
|
||||||
|
const destNationId = resolveDestNationId(ctx);
|
||||||
|
if (destNationId !== undefined) {
|
||||||
|
reqs.push({ kind: 'destNation', id: destNationId });
|
||||||
|
}
|
||||||
|
return reqs;
|
||||||
|
},
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | null;
|
||||||
|
const map = view.get({ kind: 'env', key: 'map' }) as {
|
||||||
|
cities: Array<{ id: number; connections: number[] }>;
|
||||||
|
} | null;
|
||||||
|
if (!cities || !map) {
|
||||||
|
return unknownOrDeny(ctx, [], '지도 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const destNationId = resolveDestNationId(ctx);
|
||||||
|
if (destNationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseNationId = ctx.nationId;
|
||||||
|
if (baseNationId === undefined) {
|
||||||
|
const general = readGeneral(ctx, view);
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [], '아국 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
baseNationId = general.nationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseNationCityIds = new Set(cities.filter((c) => c.nationId === baseNationId).map((c) => c.id));
|
||||||
|
const destNationCityIds = new Set(cities.filter((c) => c.nationId === destNationId).map((c) => c.id));
|
||||||
|
|
||||||
|
if (baseNationCityIds.size === 0) {
|
||||||
|
return { kind: 'deny', reason: '아국 도시가 없습니다.' };
|
||||||
|
}
|
||||||
|
if (destNationCityIds.size === 0) {
|
||||||
|
return { kind: 'deny', reason: '상대 국가 도시가 없습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cityId of baseNationCityIds) {
|
||||||
|
const cityDef = map.cities.find((c) => c.id === cityId);
|
||||||
|
if (!cityDef) continue;
|
||||||
|
for (const neighborId of cityDef.connections) {
|
||||||
|
if (destNationCityIds.has(neighborId)) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kind: 'deny', reason: '인접 국가가 아닙니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
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 LastStandAction } from '../../../src/actions/turn/nation/che_필사즉생.js';
|
||||||
|
import { LogCategory, LogScope } from '../../../src/logging/types.js';
|
||||||
|
import type { MapDefinition } from '../../../src/world/types.js';
|
||||||
|
import type { TurnSchedule } from '../../../src/turn/calendar.js';
|
||||||
|
|
||||||
|
const buildGeneral = (id: number, nationId: number, cityId: number, name = 'TestGeneral'): General => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
nationId,
|
||||||
|
cityId,
|
||||||
|
troopId: 0,
|
||||||
|
stats: {
|
||||||
|
leadership: 70,
|
||||||
|
strength: 70,
|
||||||
|
intelligence: 70,
|
||||||
|
},
|
||||||
|
experience: 100,
|
||||||
|
dedication: 100,
|
||||||
|
officerLevel: 3,
|
||||||
|
role: {
|
||||||
|
personality: null,
|
||||||
|
specialDomestic: null,
|
||||||
|
specialWar: null,
|
||||||
|
items: {
|
||||||
|
horse: null,
|
||||||
|
weapon: null,
|
||||||
|
book: null,
|
||||||
|
item: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
injury: 0,
|
||||||
|
gold: 1000,
|
||||||
|
rice: 2000,
|
||||||
|
crew: 1500,
|
||||||
|
crewTypeId: 100,
|
||||||
|
train: 80,
|
||||||
|
atmos: 80,
|
||||||
|
age: 25,
|
||||||
|
npcState: 0,
|
||||||
|
triggerState: {
|
||||||
|
flags: {},
|
||||||
|
counters: {},
|
||||||
|
modifiers: {},
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildCity = (id: number, nationId: number): City => ({
|
||||||
|
id,
|
||||||
|
name: `City${id}`,
|
||||||
|
nationId,
|
||||||
|
level: 2,
|
||||||
|
state: 0,
|
||||||
|
population: 10000,
|
||||||
|
populationMax: 10000,
|
||||||
|
agriculture: 1000,
|
||||||
|
agricultureMax: 1000,
|
||||||
|
commerce: 1000,
|
||||||
|
commerceMax: 1000,
|
||||||
|
security: 1000,
|
||||||
|
securityMax: 1000,
|
||||||
|
supplyState: 1,
|
||||||
|
frontState: 0,
|
||||||
|
defence: 200,
|
||||||
|
defenceMax: 400,
|
||||||
|
wall: 200,
|
||||||
|
wallMax: 400,
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildNation = (id: number, name = 'Nation'): Nation => ({
|
||||||
|
id,
|
||||||
|
name: `${name}${id}`,
|
||||||
|
color: '#000000',
|
||||||
|
capitalCityId: id,
|
||||||
|
chiefGeneralId: id === 1 ? 1 : null,
|
||||||
|
gold: 5000,
|
||||||
|
rice: 5000,
|
||||||
|
power: 0,
|
||||||
|
level: 1,
|
||||||
|
typeCode: 'test',
|
||||||
|
meta: {
|
||||||
|
tech: 1000,
|
||||||
|
strategic_cmd_limit: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const schedule: TurnSchedule = {
|
||||||
|
entries: [{ startMinute: 0, tickMinutes: 60 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Nation Actions', () => {
|
||||||
|
describe('che_선전포고 (Declare War)', () => {
|
||||||
|
it('succeeds with neighbor and year 2', () => {
|
||||||
|
const nation1 = buildNation(1);
|
||||||
|
const nation2 = buildNation(2);
|
||||||
|
const city1 = buildCity(1, 1);
|
||||||
|
const city2 = buildCity(2, 2);
|
||||||
|
const general = buildGeneral(1, 1, 1);
|
||||||
|
nation1.chiefGeneralId = general.id;
|
||||||
|
|
||||||
|
const definition = new DeclareWarAction();
|
||||||
|
const context = {
|
||||||
|
general,
|
||||||
|
city: city1,
|
||||||
|
nation: nation1,
|
||||||
|
destNation: nation2,
|
||||||
|
cities: [city1, city2],
|
||||||
|
nations: [nation1, nation2],
|
||||||
|
rng: {} as any,
|
||||||
|
addLog: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolution = resolveGeneralAction(
|
||||||
|
definition,
|
||||||
|
context as any,
|
||||||
|
{ now: new Date(), schedule },
|
||||||
|
{ destNationId: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolution.effects).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'diplomacy:patch',
|
||||||
|
srcNationId: 1,
|
||||||
|
destNationId: 2,
|
||||||
|
patch: expect.objectContaining({ state: 1 }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(resolution.logs.some((l) => l.scope === LogScope.SYSTEM && l.category === LogCategory.ACTION)).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(resolution.logs.some((l) => l.text.includes('【국메】'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if not neighbor', () => {
|
||||||
|
const nation1 = buildNation(1);
|
||||||
|
const nation2 = buildNation(2);
|
||||||
|
const city1 = buildCity(1, 1);
|
||||||
|
const city2 = buildCity(99, 2); // city 99 is not connected to city 1
|
||||||
|
|
||||||
|
const isolatedMap: MapDefinition = {
|
||||||
|
id: 'test',
|
||||||
|
name: 'test',
|
||||||
|
cities: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'City1',
|
||||||
|
connections: [],
|
||||||
|
level: 1,
|
||||||
|
region: 1,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
max: {} as any,
|
||||||
|
initial: {} as any,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 99,
|
||||||
|
name: 'City2',
|
||||||
|
connections: [],
|
||||||
|
level: 1,
|
||||||
|
region: 1,
|
||||||
|
position: { x: 1, y: 1 },
|
||||||
|
max: {} as any,
|
||||||
|
initial: {} as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const definition = new DeclareWarAction();
|
||||||
|
const constraints = definition.buildConstraints(
|
||||||
|
{
|
||||||
|
actorId: 1,
|
||||||
|
nationId: 1,
|
||||||
|
destNationId: 2,
|
||||||
|
env: {
|
||||||
|
relYear: 1,
|
||||||
|
map: isolatedMap,
|
||||||
|
cities: [city1, city2],
|
||||||
|
},
|
||||||
|
args: { destNationId: 2 },
|
||||||
|
mode: 'full',
|
||||||
|
} as any,
|
||||||
|
{ destNationId: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const nearNationConstraint = constraints.find((c) => c.name === 'nearNation');
|
||||||
|
expect(nearNationConstraint).toBeDefined();
|
||||||
|
|
||||||
|
// NearNation needs StateView
|
||||||
|
const view = {
|
||||||
|
has: () => true,
|
||||||
|
get: (req: any) => {
|
||||||
|
if (req.kind === 'env' && req.key === 'cities') return [city1, city2];
|
||||||
|
if (req.kind === 'env' && req.key === 'map') return isolatedMap;
|
||||||
|
if (req.kind === 'destNation') return nation2;
|
||||||
|
if (req.kind === 'nation') return nation1;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = nearNationConstraint!.test(
|
||||||
|
{
|
||||||
|
actorId: 1,
|
||||||
|
nationId: 1,
|
||||||
|
destNationId: 2,
|
||||||
|
env: { relYear: 1, map: isolatedMap, cities: [city1, city2] },
|
||||||
|
args: { destNationId: 2 },
|
||||||
|
mode: 'full',
|
||||||
|
} as any,
|
||||||
|
view as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.kind).toBe('deny');
|
||||||
|
if (result.kind === 'deny') {
|
||||||
|
expect(result.reason).toBe('인접 국가가 아닙니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('che_필사즉생 (Last Stand)', () => {
|
||||||
|
it('increases experience and sets strategic limit', () => {
|
||||||
|
const nation = buildNation(1);
|
||||||
|
const general = buildGeneral(1, 1, 1);
|
||||||
|
const definition = new LastStandAction([]);
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
general,
|
||||||
|
nation,
|
||||||
|
nationGenerals: [general],
|
||||||
|
addLog: () => {},
|
||||||
|
rng: {} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
definition.resolve(context as any, {});
|
||||||
|
|
||||||
|
expect(general.experience).toBeGreaterThan(100);
|
||||||
|
expect(nation.meta.strategic_cmd_limit).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -181,6 +181,9 @@ describe('Diplomacy Scenario', () => {
|
|||||||
commandKey: 'che_선전포고',
|
commandKey: 'che_선전포고',
|
||||||
resolver: declareWarDef,
|
resolver: declareWarDef,
|
||||||
args: { destNationId: NATION_B_ID },
|
args: { destNationId: NATION_B_ID },
|
||||||
|
context: {
|
||||||
|
destNation: mockNationB,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user