feat: Add new strategic actions for nation turns
- Implemented '백성동원' (Mobilize People) action to enhance city defense and gain experience for generals. - Added '수몰' (Flood) action to damage enemy city defenses and affect diplomatic relations. - Created '이호경식' (Degrade Relations) action to manipulate diplomatic states between nations. - Introduced '필사즉생' (Desperate Fight) action to boost training and atmosphere for generals in a nation. - Developed '허보' (Deception) action to mislead enemy generals and alter their city assignments.
This commit is contained in:
@@ -129,6 +129,8 @@ class MemoryStateView implements StateView {
|
||||
return `destNation:${req.id}`;
|
||||
case 'diplomacy':
|
||||
return `diplomacy:${req.srcNationId}:${req.destNationId}`;
|
||||
case 'diplomacyList':
|
||||
return 'diplomacy:list';
|
||||
case 'arg':
|
||||
return `arg:${req.key}`;
|
||||
case 'env':
|
||||
|
||||
@@ -351,6 +351,8 @@ class WorldStateView implements StateView {
|
||||
req.srcNationId,
|
||||
req.destNationId
|
||||
);
|
||||
case 'diplomacyList':
|
||||
return this.world.listDiplomacy();
|
||||
case 'arg':
|
||||
return this.args[req.key] ?? null;
|
||||
case 'env':
|
||||
|
||||
@@ -37,6 +37,14 @@ export interface ActionContextWorldRef {
|
||||
toNationId: number;
|
||||
state: number;
|
||||
}>;
|
||||
getDiplomacyEntry(fromNationId: number, toNationId: number): {
|
||||
fromNationId: number;
|
||||
toNationId: number;
|
||||
state: number;
|
||||
term: number;
|
||||
dead?: number;
|
||||
meta?: Record<string, unknown>;
|
||||
} | null;
|
||||
getGeneralById(id: number): ActionContextGeneral | null;
|
||||
getCityById(id: number): City | null;
|
||||
getNationById(id: number): Nation | null;
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyWithTerm,
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
existsDestNation,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} 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 type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { buildDefaultDiplomacy } from '../../../diplomacy/index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface RaidArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
|
||||
export interface RaidResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
diplomacy: { state: number; term: number };
|
||||
reverseDiplomacy: { state: number; term: number };
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
destNationGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '급습';
|
||||
const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 0;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const TERM_REDUCE = 3;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
// 급습 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getGlobalDelay(context: RaidResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 급습 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState, RaidArgs> {
|
||||
readonly key = 'che_급습';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RaidResolveContext<TriggerState>,
|
||||
_args: RaidArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const nationName = nation?.name ?? '아국';
|
||||
const destNationName = context.destNation.name;
|
||||
const actionJosa = JosaUtil.pick(ACTION_NAME, '을');
|
||||
const broadcastMessage = `<Y>${generalName}</>${generalJosa} <G><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동하였습니다.`;
|
||||
|
||||
general.experience += EXP_DED_GAIN;
|
||||
general.dedication += EXP_DED_GAIN;
|
||||
|
||||
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
|
||||
context.addLog(
|
||||
`<D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||
{
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createDiplomacyPatchEffect(
|
||||
general.nationId,
|
||||
context.destNation.id,
|
||||
{
|
||||
term: context.diplomacy.term - TERM_REDUCE,
|
||||
}
|
||||
),
|
||||
createDiplomacyPatchEffect(
|
||||
context.destNation.id,
|
||||
general.nationId,
|
||||
{
|
||||
term: context.reverseDiplomacy.term - TERM_REDUCE,
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
for (const target of context.friendlyGenerals) {
|
||||
if (target.id === general.id) {
|
||||
continue;
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const destBroadcast = `아국에 <M>${ACTION_NAME}</>${JosaUtil.pick(ACTION_NAME, '이')} 발동되었습니다.`;
|
||||
for (const target of context.destNationGenerals) {
|
||||
effects.push(
|
||||
createLogEffect(destBroadcast, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (nation) {
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
})
|
||||
);
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<D><b>${nationName}</b></>의 <Y>${generalName}</>${generalJosa} 아국에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: context.destNation.id,
|
||||
format: LogFormat.PLAIN,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 급습 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
RaidArgs,
|
||||
RaidResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_급습';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): RaidArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: RaidArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
existsDestNation(),
|
||||
allowDiplomacyWithTerm(
|
||||
1,
|
||||
12,
|
||||
'선포 12개월 이상인 상대국에만 가능합니다.'
|
||||
),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RaidResolveContext<TriggerState>,
|
||||
args: RaidArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 국가/외교 정보를 구성한다.
|
||||
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;
|
||||
}
|
||||
const diplomacy =
|
||||
worldRef.getDiplomacyEntry(base.general.nationId, destNationId) ??
|
||||
buildDefaultDiplomacy(base.general.nationId, destNationId);
|
||||
const reverseDiplomacy =
|
||||
worldRef.getDiplomacyEntry(destNationId, base.general.nationId) ??
|
||||
buildDefaultDiplomacy(destNationId, base.general.nationId);
|
||||
const generals = worldRef.listGenerals();
|
||||
const friendlyGenerals = generals.filter(
|
||||
(general) => general.nationId === base.general.nationId
|
||||
);
|
||||
const destNationGenerals = generals.filter(
|
||||
(general) => general.nationId === destNationId
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
diplomacy: { state: diplomacy.state, term: diplomacy.term },
|
||||
reverseDiplomacy: { state: reverseDiplomacy.state, term: reverseDiplomacy.term },
|
||||
friendlyGenerals,
|
||||
destNationGenerals,
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_급습',
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
occupiedCity,
|
||||
occupiedDestCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createCityPatchEffect, createLogEffect } 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';
|
||||
|
||||
export interface MobilizePeopleArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
|
||||
export interface MobilizePeopleResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '백성동원';
|
||||
const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 0;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const DEFENCE_RATE = 0.8;
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
// 백성동원 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getGlobalDelay(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 백성동원 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState, MobilizePeopleArgs> {
|
||||
readonly key = 'che_백성동원';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: MobilizePeopleResolveContext<TriggerState>,
|
||||
_args: MobilizePeopleArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const cityName = context.destCity.name;
|
||||
const broadcastMessage = `<Y>${generalName}</>${generalJosa} <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 하였습니다.`;
|
||||
|
||||
general.experience += EXP_DED_GAIN;
|
||||
general.dedication += EXP_DED_GAIN;
|
||||
|
||||
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
|
||||
context.addLog(
|
||||
`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`,
|
||||
{
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
for (const target of context.friendlyGenerals) {
|
||||
if (target.id === general.id) {
|
||||
continue;
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const nextDefence = Math.max(
|
||||
context.destCity.defence,
|
||||
context.destCity.defenceMax * DEFENCE_RATE
|
||||
);
|
||||
const nextWall = Math.max(
|
||||
context.destCity.wall,
|
||||
context.destCity.wallMax * DEFENCE_RATE
|
||||
);
|
||||
effects.push(
|
||||
createCityPatchEffect(
|
||||
{
|
||||
defence: nextDefence,
|
||||
wall: nextWall,
|
||||
},
|
||||
context.destCity.id
|
||||
)
|
||||
);
|
||||
|
||||
if (nation) {
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 백성동원 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
MobilizePeopleArgs,
|
||||
MobilizePeopleResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_백성동원';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): MobilizePeopleArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: MobilizePeopleArgs
|
||||
): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
occupiedDestCity(),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: MobilizePeopleResolveContext<TriggerState>,
|
||||
args: MobilizePeopleArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시/장수 정보를 구성한다.
|
||||
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);
|
||||
if (!destCity) {
|
||||
return null;
|
||||
}
|
||||
const friendlyGenerals = worldRef
|
||||
.listGenerals()
|
||||
.filter((general) => general.nationId === base.general.nationId);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
friendlyGenerals,
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_백성동원',
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
@@ -0,0 +1,276 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
notNeutralDestCity,
|
||||
notOccupiedDestCity,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createCityPatchEffect, createLogEffect } 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';
|
||||
|
||||
export interface FloodArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
|
||||
export interface FloodResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation: Nation | null;
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
destNationGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '수몰';
|
||||
const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 2;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const DAMAGE_RATE = 0.2;
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
// 수몰 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getGlobalDelay(context: FloodResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 수몰 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState, FloodArgs> {
|
||||
readonly key = 'che_수몰';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: FloodResolveContext<TriggerState>,
|
||||
_args: FloodArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const cityName = context.destCity.name;
|
||||
const broadcastMessage = `<Y>${generalName}</>${generalJosa} <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동하였습니다.`;
|
||||
const destBroadcastMessage = `<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>이 발동되었습니다.`;
|
||||
|
||||
general.experience += EXP_DED_GAIN;
|
||||
general.dedication += EXP_DED_GAIN;
|
||||
|
||||
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
|
||||
context.addLog(
|
||||
`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`,
|
||||
{
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
for (const target of context.friendlyGenerals) {
|
||||
if (target.id === general.id) {
|
||||
continue;
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const target of context.destNationGenerals) {
|
||||
effects.push(
|
||||
createLogEffect(destBroadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
effects.push(
|
||||
createCityPatchEffect(
|
||||
{
|
||||
defence: context.destCity.defence * DAMAGE_RATE,
|
||||
wall: context.destCity.wall * DAMAGE_RATE,
|
||||
},
|
||||
context.destCity.id
|
||||
)
|
||||
);
|
||||
|
||||
if (nation) {
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (context.destNation) {
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<D><b>${nation?.name ?? '상대국'}</b></>의 <Y>${generalName}</>${generalJosa} 아국의 <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: context.destNation.id,
|
||||
format: LogFormat.PLAIN,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 수몰 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
FloodArgs,
|
||||
FloodResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_수몰';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): FloodArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: FloodArgs
|
||||
): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
notNeutralDestCity(),
|
||||
notOccupiedDestCity(),
|
||||
allowDiplomacyBetweenStatus([0], '교전중인 국가의 도시가 아닙니다.'),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: FloodResolveContext<TriggerState>,
|
||||
args: FloodArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시/장수 정보를 구성한다.
|
||||
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);
|
||||
if (!destCity) {
|
||||
return null;
|
||||
}
|
||||
const destNation = worldRef.getNationById(destCity.nationId);
|
||||
const generals = worldRef.listGenerals();
|
||||
const friendlyGenerals = generals.filter(
|
||||
(general) => general.nationId === base.general.nationId
|
||||
);
|
||||
const destNationGenerals = generals.filter(
|
||||
(general) => general.nationId === destCity.nationId
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
destNation: destNation ?? null,
|
||||
friendlyGenerals,
|
||||
destNationGenerals,
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_수몰',
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
existsDestNation,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} 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 type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import {
|
||||
buildDefaultDiplomacy,
|
||||
DIPLOMACY_STATE,
|
||||
} from '../../../diplomacy/index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface DegradeRelationsArgs {
|
||||
destNationId: number;
|
||||
}
|
||||
|
||||
export interface DegradeRelationsResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
diplomacy: { state: number; term: number };
|
||||
reverseDiplomacy: { state: number; term: number };
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
destNationGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '이호경식';
|
||||
const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 0;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const resolveNextTerm = (state: number, term: number): number =>
|
||||
state === DIPLOMACY_STATE.WAR ? 3 : term + 3;
|
||||
|
||||
// 이호경식 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 이호경식 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState, DegradeRelationsArgs> {
|
||||
readonly key = 'che_이호경식';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DegradeRelationsResolveContext<TriggerState>,
|
||||
_args: DegradeRelationsArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const nationName = nation?.name ?? '아국';
|
||||
const nationJosa = JosaUtil.pick(nationName, '이');
|
||||
const destNationName = context.destNation.name;
|
||||
const actionJosa = JosaUtil.pick(ACTION_NAME, '을');
|
||||
const broadcastMessage = `<Y>${generalName}</>${generalJosa} <G><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동하였습니다.`;
|
||||
|
||||
general.experience += EXP_DED_GAIN;
|
||||
general.dedication += EXP_DED_GAIN;
|
||||
|
||||
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
|
||||
context.addLog(
|
||||
`<D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||
{
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createDiplomacyPatchEffect(
|
||||
general.nationId,
|
||||
context.destNation.id,
|
||||
{
|
||||
state: DIPLOMACY_STATE.DECLARATION,
|
||||
term: resolveNextTerm(
|
||||
context.diplomacy.state,
|
||||
context.diplomacy.term
|
||||
),
|
||||
}
|
||||
),
|
||||
createDiplomacyPatchEffect(
|
||||
context.destNation.id,
|
||||
general.nationId,
|
||||
{
|
||||
state: DIPLOMACY_STATE.DECLARATION,
|
||||
term: resolveNextTerm(
|
||||
context.reverseDiplomacy.state,
|
||||
context.reverseDiplomacy.term
|
||||
),
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
for (const target of context.friendlyGenerals) {
|
||||
if (target.id === general.id) {
|
||||
continue;
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const destBroadcast = `<D><b>${nationName}</b></>${nationJosa} 아국에 <M>${ACTION_NAME}</>${actionJosa} 발동하였습니다.`;
|
||||
for (const target of context.destNationGenerals) {
|
||||
effects.push(
|
||||
createLogEffect(destBroadcast, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (nation) {
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
})
|
||||
);
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<D><b>${nationName}</b></>의 <Y>${generalName}</>${generalJosa} 아국에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: context.destNation.id,
|
||||
format: LogFormat.PLAIN,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 이호경식 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DegradeRelationsArgs,
|
||||
DegradeRelationsResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_이호경식';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DegradeRelationsArgs | null {
|
||||
const data = raw as { destNationId?: unknown };
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
if (destNationId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DegradeRelationsArgs
|
||||
): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
existsDestNation(),
|
||||
allowDiplomacyBetweenStatus(
|
||||
[0, 1],
|
||||
'선포, 전쟁중인 상대국에게만 가능합니다.'
|
||||
),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DegradeRelationsResolveContext<TriggerState>,
|
||||
args: DegradeRelationsArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 국가/외교 정보를 구성한다.
|
||||
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;
|
||||
}
|
||||
const diplomacy =
|
||||
worldRef.getDiplomacyEntry(base.general.nationId, destNationId) ??
|
||||
buildDefaultDiplomacy(base.general.nationId, destNationId);
|
||||
const reverseDiplomacy =
|
||||
worldRef.getDiplomacyEntry(destNationId, base.general.nationId) ??
|
||||
buildDefaultDiplomacy(destNationId, base.general.nationId);
|
||||
const generals = worldRef.listGenerals();
|
||||
const friendlyGenerals = generals.filter(
|
||||
(general) => general.nationId === base.general.nationId
|
||||
);
|
||||
const destNationGenerals = generals.filter(
|
||||
(general) => general.nationId === destNationId
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
diplomacy: { state: diplomacy.state, term: diplomacy.term },
|
||||
reverseDiplomacy: { state: reverseDiplomacy.state, term: reverseDiplomacy.term },
|
||||
friendlyGenerals,
|
||||
destNationGenerals,
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_이호경식',
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyStatus,
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createGeneralPatchEffect, createLogEffect } 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';
|
||||
|
||||
export interface DesperateFightArgs {}
|
||||
|
||||
export interface DesperateFightResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
nationGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '필사즉생';
|
||||
const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 2;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const TRAIN_CAP = 100;
|
||||
const ATMOS_CAP = 100;
|
||||
|
||||
// 필사즉생 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DesperateFightResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 필사즉생 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState, DesperateFightArgs> {
|
||||
readonly key = 'che_필사즉생';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DesperateFightResolveContext<TriggerState>,
|
||||
_args: DesperateFightArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const broadcastMessage = `<Y>${generalName}</>${generalJosa} <M>${ACTION_NAME}</>을 발동하였습니다.`;
|
||||
|
||||
general.experience += EXP_DED_GAIN;
|
||||
general.dedication += EXP_DED_GAIN;
|
||||
|
||||
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
|
||||
context.addLog(`<M>${ACTION_NAME}</>을 발동`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
});
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
const updateTrainAtmos = (
|
||||
target: General<TriggerState>
|
||||
): { train: number; atmos: number } | null => {
|
||||
const nextTrain = Math.max(target.train, TRAIN_CAP);
|
||||
const nextAtmos = Math.max(target.atmos, ATMOS_CAP);
|
||||
if (nextTrain === target.train && nextAtmos === target.atmos) {
|
||||
return null;
|
||||
}
|
||||
return { train: nextTrain, atmos: nextAtmos };
|
||||
};
|
||||
|
||||
const selfPatch = updateTrainAtmos(general);
|
||||
if (selfPatch) {
|
||||
general.train = selfPatch.train;
|
||||
general.atmos = selfPatch.atmos;
|
||||
}
|
||||
|
||||
for (const target of context.nationGenerals) {
|
||||
if (target.id === general.id) {
|
||||
continue;
|
||||
}
|
||||
const patch = updateTrainAtmos(target);
|
||||
if (patch) {
|
||||
effects.push(
|
||||
createGeneralPatchEffect(patch, target.id)
|
||||
);
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (nation) {
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 필사즉생 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DesperateFightArgs,
|
||||
DesperateFightResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_필사즉생';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
}
|
||||
|
||||
parseArgs(_raw: unknown): DesperateFightArgs | null {
|
||||
void _raw;
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DesperateFightArgs
|
||||
): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
allowDiplomacyStatus([0], '전쟁중이 아닙니다.'),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DesperateFightResolveContext<TriggerState>,
|
||||
args: DesperateFightArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 국가 장수 목록을 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
const worldRef = options.worldRef;
|
||||
if (!worldRef) {
|
||||
return null;
|
||||
}
|
||||
const nationGenerals = worldRef
|
||||
.listGenerals()
|
||||
.filter((entry) => entry.nationId === base.general.nationId);
|
||||
return {
|
||||
...base,
|
||||
nationGenerals,
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_필사즉생',
|
||||
category: '전략',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
@@ -0,0 +1,304 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
notNeutralDestCity,
|
||||
notOccupiedDestCity,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createGeneralPatchEffect, createLogEffect } 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';
|
||||
|
||||
export interface DeceptionArgs {
|
||||
destCityId: number;
|
||||
}
|
||||
|
||||
export interface DeceptionResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation: Nation | null;
|
||||
destCityGenerals: Array<General<TriggerState>>;
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
destNationSupplyCities: City[];
|
||||
}
|
||||
|
||||
const ACTION_NAME = '허보';
|
||||
const DEFAULT_GLOBAL_DELAY = 9;
|
||||
const PRE_REQ_TURN = 1;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
|
||||
const parseCityId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const value = Math.floor(raw);
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const pickMoveCityId = (
|
||||
rng: GeneralActionResolveContext['rng'],
|
||||
destCityId: number,
|
||||
candidates: City[]
|
||||
): number => {
|
||||
if (candidates.length === 0) {
|
||||
return destCityId;
|
||||
}
|
||||
let idx = rng.nextInt(0, candidates.length);
|
||||
let cityId = candidates[idx]?.id ?? destCityId;
|
||||
if (cityId === destCityId && candidates.length > 1) {
|
||||
idx = rng.nextInt(0, candidates.length);
|
||||
cityId = candidates[idx]?.id ?? destCityId;
|
||||
}
|
||||
return cityId;
|
||||
};
|
||||
|
||||
// 허보 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DeceptionResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 허보 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState, DeceptionArgs> {
|
||||
readonly key = 'che_허보';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DeceptionResolveContext<TriggerState>,
|
||||
_args: DeceptionArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const cityName = context.destCity.name;
|
||||
const broadcastMessage = `<Y>${generalName}</>${generalJosa} <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동하였습니다.`;
|
||||
const destBroadcastMessage = `상대의 <M>${ACTION_NAME}</>에 당했다!`;
|
||||
|
||||
general.experience += EXP_DED_GAIN;
|
||||
general.dedication += EXP_DED_GAIN;
|
||||
|
||||
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
|
||||
context.addLog(
|
||||
`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동`,
|
||||
{
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
for (const target of context.friendlyGenerals) {
|
||||
if (target.id === general.id) {
|
||||
continue;
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const target of context.destCityGenerals) {
|
||||
const moveCityId = pickMoveCityId(
|
||||
context.rng,
|
||||
context.destCity.id,
|
||||
context.destNationSupplyCities
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(destBroadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: target.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
if (moveCityId !== target.cityId) {
|
||||
effects.push(
|
||||
createGeneralPatchEffect(
|
||||
{ cityId: moveCityId },
|
||||
target.id
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (nation) {
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
nation.meta = {
|
||||
...(nation.meta as object),
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (context.destNation) {
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<D><b>${nation?.name ?? '상대국'}</b></>의 <Y>${generalName}</>${generalJosa} 아국의 <G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: context.destNation.id,
|
||||
format: LogFormat.PLAIN,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
// 허보 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<TriggerState, DeceptionArgs, DeceptionResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_허보';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.resolver = new ActionResolver(modules);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DeceptionArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
const destCityId = parseCityId(data?.destCityId);
|
||||
if (destCityId === null) {
|
||||
return null;
|
||||
}
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DeceptionArgs
|
||||
): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
notNeutralDestCity(),
|
||||
notOccupiedDestCity(),
|
||||
allowDiplomacyBetweenStatus(
|
||||
[0, 1],
|
||||
'선포, 전쟁중인 상대국에게만 가능합니다.'
|
||||
),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DeceptionResolveContext<TriggerState>,
|
||||
args: DeceptionArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
// 예약 턴 실행에 필요한 대상 도시/장수 정보를 구성한다.
|
||||
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);
|
||||
if (!destCity) {
|
||||
return null;
|
||||
}
|
||||
const destNation = worldRef.getNationById(destCity.nationId);
|
||||
const generals = worldRef.listGenerals();
|
||||
const destCityGenerals = generals.filter(
|
||||
(general) =>
|
||||
general.nationId === destCity.nationId &&
|
||||
general.cityId === destCity.id
|
||||
);
|
||||
const friendlyGenerals = generals.filter(
|
||||
(general) => general.nationId === base.general.nationId
|
||||
);
|
||||
const destNationSupplyCities = worldRef
|
||||
.listCities()
|
||||
.filter(
|
||||
(city) =>
|
||||
city.nationId === destCity.nationId && city.supplyState > 0
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
destNation: destNation ?? null,
|
||||
destCityGenerals,
|
||||
friendlyGenerals,
|
||||
destNationSupplyCities,
|
||||
};
|
||||
};
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_허보',
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
@@ -8,6 +8,12 @@ export const NATION_TURN_COMMAND_KEYS = [
|
||||
'che_불가침제의',
|
||||
'che_불가침파기제의',
|
||||
'che_의병모집',
|
||||
'che_허보',
|
||||
'che_필사즉생',
|
||||
'che_백성동원',
|
||||
'che_이호경식',
|
||||
'che_수몰',
|
||||
'che_급습',
|
||||
] as const;
|
||||
|
||||
export type NationTurnCommandKey =
|
||||
@@ -32,6 +38,12 @@ const defaultImporters: Record<
|
||||
che_불가침제의: async () => import('./che_불가침제의.js'),
|
||||
che_불가침파기제의: async () => import('./che_불가침파기제의.js'),
|
||||
che_의병모집: async () => import('./che_의병모집.js'),
|
||||
che_허보: async () => import('./che_허보.js'),
|
||||
che_필사즉생: async () => import('./che_필사즉생.js'),
|
||||
che_백성동원: async () => import('./che_백성동원.js'),
|
||||
che_이호경식: async () => import('./che_이호경식.js'),
|
||||
che_수몰: async () => import('./che_수몰.js'),
|
||||
che_급습: async () => import('./che_급습.js'),
|
||||
};
|
||||
|
||||
export const isNationTurnCommandKey = (
|
||||
|
||||
@@ -9,6 +9,37 @@ import {
|
||||
} from './helpers.js';
|
||||
import type { Constraint, RequirementKey } from './types.js';
|
||||
|
||||
const readDiplomacyEntry = (
|
||||
view: { has(req: RequirementKey): boolean; get(req: RequirementKey): unknown },
|
||||
srcNationId: number,
|
||||
destNationId: number
|
||||
): { state: number | null; term: number | null } | null => {
|
||||
const req: RequirementKey = {
|
||||
kind: 'diplomacy',
|
||||
srcNationId,
|
||||
destNationId,
|
||||
};
|
||||
if (!view.has(req)) {
|
||||
return null;
|
||||
}
|
||||
const value = view.get(req);
|
||||
if (typeof value === 'number') {
|
||||
return { state: value, term: null };
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
const record = value as { state?: number; stateCode?: number; term?: number };
|
||||
const state =
|
||||
typeof record.state === 'number'
|
||||
? record.state
|
||||
: typeof record.stateCode === 'number'
|
||||
? record.stateCode
|
||||
: null;
|
||||
const term = typeof record.term === 'number' ? record.term : null;
|
||||
return { state, term };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const disallowDiplomacyBetweenStatus = (
|
||||
disallowList: Record<number, string>
|
||||
): Constraint => ({
|
||||
@@ -118,3 +149,104 @@ export const allowDiplomacyBetweenStatus = (
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const allowDiplomacyWithTerm = (
|
||||
requiredState: number,
|
||||
minTerm: number,
|
||||
reason: string
|
||||
): Constraint => ({
|
||||
name: 'AllowDiplomacyWithTerm',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||
}
|
||||
const destNationId = resolveDestNationId(ctx);
|
||||
if (destNationId !== undefined) {
|
||||
reqs.push({ kind: 'destNation', id: destNationId });
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({
|
||||
kind: 'diplomacy',
|
||||
srcNationId: ctx.nationId,
|
||||
destNationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
if (destCityId !== undefined) {
|
||||
reqs.push({ kind: 'destCity', id: destCityId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const general = readGeneral(ctx, view);
|
||||
const baseNationId = ctx.nationId ?? general?.nationId;
|
||||
if (baseNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const destCity = readDestCity(ctx, view);
|
||||
const destNationId =
|
||||
resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
if (destNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
|
||||
}
|
||||
const entry = readDiplomacyEntry(view, baseNationId, destNationId);
|
||||
if (!entry || entry.state === null || entry.term === null) {
|
||||
const req: RequirementKey = {
|
||||
kind: 'diplomacy',
|
||||
srcNationId: baseNationId,
|
||||
destNationId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
|
||||
}
|
||||
if (entry.state !== requiredState || entry.term < minTerm) {
|
||||
return { kind: 'deny', reason };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const allowDiplomacyStatus = (
|
||||
allowList: number[],
|
||||
reason: string
|
||||
): Constraint => ({
|
||||
name: 'AllowDiplomacyStatus',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||
}
|
||||
reqs.push({ kind: 'diplomacyList' });
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const general = readGeneral(ctx, view);
|
||||
const baseNationId = ctx.nationId ?? general?.nationId;
|
||||
if (baseNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = { kind: 'diplomacyList' };
|
||||
if (!view.has(req)) {
|
||||
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
|
||||
}
|
||||
const list = view.get(req);
|
||||
if (!Array.isArray(list)) {
|
||||
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
|
||||
}
|
||||
const matched = list.some((entry) => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const record = entry as { fromNationId?: number; state?: number };
|
||||
return (
|
||||
record.fromNationId === baseNationId &&
|
||||
typeof record.state === 'number' &&
|
||||
allowList.includes(record.state)
|
||||
);
|
||||
});
|
||||
if (!matched) {
|
||||
return { kind: 'deny', reason };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export type RequirementKey =
|
||||
| { kind: 'destCity'; id: number }
|
||||
| { kind: 'destNation'; id: number }
|
||||
| { kind: 'diplomacy'; srcNationId: number; destNationId: number }
|
||||
| { kind: 'diplomacyList' }
|
||||
| { kind: 'arg'; key: string }
|
||||
| { kind: 'env'; key: string };
|
||||
|
||||
|
||||
@@ -23,7 +23,14 @@ export type TriggerDomesticVarType =
|
||||
| 'rice'
|
||||
| 'probability';
|
||||
|
||||
export type TriggerStrategicActionType = '의병모집';
|
||||
export type TriggerStrategicActionType =
|
||||
| '의병모집'
|
||||
| '허보'
|
||||
| '필사즉생'
|
||||
| '백성동원'
|
||||
| '이호경식'
|
||||
| '수몰'
|
||||
| '급습';
|
||||
|
||||
export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
"che_선전포고",
|
||||
"che_불가침제의",
|
||||
"che_불가침파기제의",
|
||||
"che_의병모집"
|
||||
"che_의병모집",
|
||||
"che_허보",
|
||||
"che_필사즉생",
|
||||
"che_백성동원",
|
||||
"che_이호경식",
|
||||
"che_수몰",
|
||||
"che_급습"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user