feat: eslint 적용 및 관련 코드 일괄 수정
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from './engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from './engine.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
|
||||
export interface GeneralActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
Args = unknown,
|
||||
Context extends GeneralActionResolveContext<TriggerState> = GeneralActionResolveContext<TriggerState>
|
||||
Context extends GeneralActionResolveContext<TriggerState> = GeneralActionResolveContext<TriggerState>,
|
||||
> {
|
||||
key: string;
|
||||
name: string;
|
||||
|
||||
@@ -11,55 +11,42 @@ import type {
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import { getNextTurnAt, type TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
|
||||
import {
|
||||
LogCategory,
|
||||
type LogEntryDraft,
|
||||
LogFormat,
|
||||
LogScope,
|
||||
} from '@sammo-ts/logic/logging/types.js';
|
||||
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
|
||||
enablePatches();
|
||||
|
||||
export interface WorldState<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface WorldState<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
general: General<TriggerState>;
|
||||
city?: City;
|
||||
nation?: Nation | null;
|
||||
}
|
||||
|
||||
export interface GeneralActionResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionContext<TriggerState> {
|
||||
rng: RandomGenerator;
|
||||
city?: City;
|
||||
nation?: Nation | null;
|
||||
addLog(
|
||||
message: string,
|
||||
options?: Partial<Omit<LogEntryDraft, 'text'>>
|
||||
): void;
|
||||
addLog(message: string, options?: Partial<Omit<LogEntryDraft, 'text'>>): void;
|
||||
}
|
||||
|
||||
export type GeneralActionResolveInputContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> = Omit<GeneralActionResolveContext<TriggerState>, 'addLog'>;
|
||||
export type GeneralActionResolveInputContext<TriggerState extends GeneralTriggerState = GeneralTriggerState> = Omit<
|
||||
GeneralActionResolveContext<TriggerState>,
|
||||
'addLog'
|
||||
>;
|
||||
|
||||
export interface TurnScheduleContext {
|
||||
now: Date;
|
||||
schedule: TurnSchedule;
|
||||
}
|
||||
|
||||
export interface GeneralPatchEffect<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface GeneralPatchEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
type: 'general:patch';
|
||||
patch: Partial<General<TriggerState>>;
|
||||
targetId?: GeneralId;
|
||||
}
|
||||
|
||||
export interface GeneralAddEffect<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface GeneralAddEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
type: 'general:add';
|
||||
general: General<TriggerState>;
|
||||
}
|
||||
@@ -99,9 +86,7 @@ export interface NextTurnOverrideEffect {
|
||||
nextTurnAt: Date;
|
||||
}
|
||||
|
||||
export type GeneralActionEffect<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> =
|
||||
export type GeneralActionEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
|
||||
| GeneralPatchEffect<TriggerState>
|
||||
| GeneralAddEffect<TriggerState>
|
||||
| CityPatchEffect
|
||||
@@ -110,21 +95,13 @@ export type GeneralActionEffect<
|
||||
| LogEffect
|
||||
| NextTurnOverrideEffect;
|
||||
|
||||
export interface GeneralActionOutcome<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
effects: GeneralActionEffect<TriggerState>[];
|
||||
}
|
||||
|
||||
export interface GeneralActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
Args = unknown
|
||||
> {
|
||||
export interface GeneralActionResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState, Args = unknown> {
|
||||
key: string;
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
args: Args
|
||||
): GeneralActionOutcome<TriggerState>;
|
||||
resolve(context: GeneralActionResolveContext<TriggerState>, args: Args): GeneralActionOutcome<TriggerState>;
|
||||
}
|
||||
|
||||
export interface GeneralActionResolution {
|
||||
@@ -152,9 +129,7 @@ export interface GeneralActionResolution {
|
||||
};
|
||||
}
|
||||
|
||||
export const createGeneralPatchEffect = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const createGeneralPatchEffect = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
patch: Partial<General<TriggerState>>,
|
||||
targetId?: GeneralId
|
||||
): GeneralPatchEffect<TriggerState> => ({
|
||||
@@ -163,28 +138,20 @@ export const createGeneralPatchEffect = <
|
||||
...(targetId !== undefined ? { targetId } : {}),
|
||||
});
|
||||
|
||||
export const createGeneralAddEffect = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const createGeneralAddEffect = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
general: General<TriggerState>
|
||||
): GeneralAddEffect<TriggerState> => ({
|
||||
type: 'general:add',
|
||||
general,
|
||||
});
|
||||
|
||||
export const createCityPatchEffect = (
|
||||
patch: Partial<City>,
|
||||
targetId?: CityId
|
||||
): CityPatchEffect => ({
|
||||
export const createCityPatchEffect = (patch: Partial<City>, targetId?: CityId): CityPatchEffect => ({
|
||||
type: 'city:patch',
|
||||
patch,
|
||||
...(targetId !== undefined ? { targetId } : {}),
|
||||
});
|
||||
|
||||
export const createNationPatchEffect = (
|
||||
patch: Partial<Nation>,
|
||||
targetId?: NationId
|
||||
): NationPatchEffect => ({
|
||||
export const createNationPatchEffect = (patch: Partial<Nation>, targetId?: NationId): NationPatchEffect => ({
|
||||
type: 'nation:patch',
|
||||
patch,
|
||||
...(targetId !== undefined ? { targetId } : {}),
|
||||
@@ -201,21 +168,14 @@ export const createDiplomacyPatchEffect = (
|
||||
patch,
|
||||
});
|
||||
|
||||
export const createLogEffect = (
|
||||
message: string,
|
||||
options: Partial<Omit<LogEntryDraft, 'text'>> = {}
|
||||
): LogEffect => ({
|
||||
export const createLogEffect = (message: string, options: Partial<Omit<LogEntryDraft, 'text'>> = {}): LogEffect => ({
|
||||
type: 'log',
|
||||
entry: {
|
||||
scope: options.scope ?? LogScope.GENERAL,
|
||||
category: options.category ?? LogCategory.ACTION,
|
||||
text: message,
|
||||
...(options.generalId !== undefined
|
||||
? { generalId: options.generalId }
|
||||
: {}),
|
||||
...(options.nationId !== undefined
|
||||
? { nationId: options.nationId }
|
||||
: {}),
|
||||
...(options.generalId !== undefined ? { generalId: options.generalId } : {}),
|
||||
...(options.nationId !== undefined ? { nationId: options.nationId } : {}),
|
||||
...(options.userId !== undefined ? { userId: options.userId } : {}),
|
||||
...(options.subType !== undefined ? { subType: options.subType } : {}),
|
||||
...(options.meta !== undefined ? { meta: options.meta } : {}),
|
||||
@@ -223,18 +183,13 @@ export const createLogEffect = (
|
||||
},
|
||||
});
|
||||
|
||||
export const createNextTurnOverrideEffect = (
|
||||
nextTurnAt: Date
|
||||
): NextTurnOverrideEffect => ({
|
||||
export const createNextTurnOverrideEffect = (nextTurnAt: Date): NextTurnOverrideEffect => ({
|
||||
type: 'schedule:override',
|
||||
nextTurnAt,
|
||||
});
|
||||
|
||||
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
|
||||
export const resolveGeneralAction = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
Args = unknown
|
||||
>(
|
||||
export const resolveGeneralAction = <TriggerState extends GeneralTriggerState = GeneralTriggerState, Args = unknown>(
|
||||
resolver: GeneralActionResolver<TriggerState, Args>,
|
||||
context: GeneralActionResolveInputContext<TriggerState>,
|
||||
scheduleContext: TurnScheduleContext,
|
||||
@@ -257,10 +212,7 @@ export const resolveGeneralAction = <
|
||||
nation: context.nation,
|
||||
} as WorldState<TriggerState>,
|
||||
(draft) => {
|
||||
const addLog = (
|
||||
message: string,
|
||||
options: Partial<Omit<LogEntryDraft, 'text'>> = {}
|
||||
) => {
|
||||
const addLog = (message: string, options: Partial<Omit<LogEntryDraft, 'text'>> = {}) => {
|
||||
const entry: LogEntryDraft = {
|
||||
scope: options.scope ?? LogScope.GENERAL,
|
||||
category: options.category ?? LogCategory.ACTION,
|
||||
@@ -363,9 +315,7 @@ export const resolveGeneralAction = <
|
||||
}
|
||||
);
|
||||
|
||||
const nextTurnAt =
|
||||
nextTurnAtOverride ??
|
||||
getNextTurnAt(scheduleContext.now, scheduleContext.schedule);
|
||||
const nextTurnAt = nextTurnAtOverride ?? getNextTurnAt(scheduleContext.now, scheduleContext.schedule);
|
||||
|
||||
const dirty: NonNullable<GeneralActionResolution['dirty']> = {
|
||||
general: false,
|
||||
@@ -396,11 +346,7 @@ export const resolveGeneralAction = <
|
||||
if (dirty.general || dirty.city || dirty.nation) {
|
||||
resolution.dirty = dirty;
|
||||
}
|
||||
if (
|
||||
patches.generals.length > 0 ||
|
||||
patches.cities.length > 0 ||
|
||||
patches.nations.length > 0
|
||||
) {
|
||||
if (patches.generals.length > 0 || patches.cities.length > 0 || patches.nations.length > 0) {
|
||||
resolution.patches = patches;
|
||||
}
|
||||
if (createdGenerals.length > 0) {
|
||||
|
||||
@@ -12,10 +12,7 @@ import {
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } 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';
|
||||
|
||||
@@ -27,7 +24,7 @@ export interface NonAggressionAcceptArgs {
|
||||
}
|
||||
|
||||
export interface NonAggressionAcceptContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
currentYear: number;
|
||||
currentMonth: number;
|
||||
@@ -65,8 +62,7 @@ const parseMonth = (raw: unknown): number | null => {
|
||||
return month >= 1 && month <= 12 ? month : null;
|
||||
};
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number =>
|
||||
year * 12 + month - 1;
|
||||
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const requireFutureTerm = (): Constraint => ({
|
||||
name: 'RequireNonAggressionFutureTerm',
|
||||
@@ -78,11 +74,9 @@ const requireFutureTerm = (): Constraint => ({
|
||||
],
|
||||
test: (ctx) => {
|
||||
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
||||
const monthValue =
|
||||
typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
||||
const envMonthValue =
|
||||
typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const missing = [];
|
||||
|
||||
if (yearValue === null) {
|
||||
@@ -126,11 +120,7 @@ const notSameDestGeneral = (): Constraint => ({
|
||||
test: (ctx) => {
|
||||
const destGeneralId = ctx.args.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return unknownOrDeny(
|
||||
ctx,
|
||||
[{ kind: 'arg', key: 'destGeneralId' }],
|
||||
'장수 정보가 없습니다.'
|
||||
);
|
||||
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneralId === ctx.actorId) {
|
||||
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
|
||||
@@ -141,12 +131,8 @@ const notSameDestGeneral = (): Constraint => ({
|
||||
|
||||
// 불가침 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
NonAggressionAcceptArgs,
|
||||
NonAggressionAcceptContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionAcceptArgs, NonAggressionAcceptContext<TriggerState>> {
|
||||
public readonly key = 'che_불가침수락';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -161,21 +147,13 @@ export class ActionDefinition<
|
||||
const destGeneralId = parseGeneralId(data?.destGeneralId);
|
||||
const year = parseYear(data?.year);
|
||||
const month = parseMonth(data?.month);
|
||||
if (
|
||||
destNationId === null ||
|
||||
destGeneralId === null ||
|
||||
year === null ||
|
||||
month === null
|
||||
) {
|
||||
if (destNationId === null || destGeneralId === null || year === null || month === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId, destGeneralId, year, month };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NonAggressionAcceptArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionAcceptArgs): Constraint[] {
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
@@ -201,22 +179,16 @@ export class ActionDefinition<
|
||||
if (nationId === undefined || nationId <= 0) {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const currentMonth = resolveMonthIndex(
|
||||
context.currentYear,
|
||||
context.currentMonth
|
||||
);
|
||||
const currentMonth = resolveMonthIndex(context.currentYear, context.currentMonth);
|
||||
const targetMonth = args.year * 12 + args.month;
|
||||
const term = Math.max(0, targetMonth - currentMonth);
|
||||
|
||||
@@ -230,14 +202,11 @@ export class ActionDefinition<
|
||||
state: DIPLOMACY_NON_AGGRESSION,
|
||||
term,
|
||||
}),
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } 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';
|
||||
|
||||
@@ -46,11 +43,7 @@ const notSameDestGeneral = (): Constraint => ({
|
||||
test: (ctx) => {
|
||||
const destGeneralId = ctx.args.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return unknownOrDeny(
|
||||
ctx,
|
||||
[{ kind: 'arg', key: 'destGeneralId' }],
|
||||
'장수 정보가 없습니다.'
|
||||
);
|
||||
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneralId === ctx.actorId) {
|
||||
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
|
||||
@@ -61,10 +54,8 @@ const notSameDestGeneral = (): Constraint => ({
|
||||
|
||||
// 불가침 파기 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements
|
||||
GeneralActionDefinition<TriggerState, NonAggressionCancelAcceptArgs>
|
||||
{
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionCancelAcceptArgs> {
|
||||
public readonly key = 'che_불가침파기수락';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -78,10 +69,7 @@ export class ActionDefinition<
|
||||
return { destNationId, destGeneralId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NonAggressionCancelAcceptArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelAcceptArgs): Constraint[] {
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
@@ -89,10 +77,7 @@ export class ActionDefinition<
|
||||
existsDestGeneral(),
|
||||
destGeneralInDestNation(),
|
||||
notSameDestGeneral(),
|
||||
allowDiplomacyBetweenStatus(
|
||||
[DIPLOMACY_NON_AGGRESSION],
|
||||
'불가침 중인 상대국에게만 가능합니다.'
|
||||
),
|
||||
allowDiplomacyBetweenStatus([DIPLOMACY_NON_AGGRESSION], '불가침 중인 상대국에게만 가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -104,14 +89,11 @@ export class ActionDefinition<
|
||||
if (nationId === undefined || nationId <= 0) {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -126,14 +108,11 @@ export class ActionDefinition<
|
||||
state: DIPLOMACY_NEUTRAL,
|
||||
term: 0,
|
||||
}),
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
Nation,
|
||||
Troop,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, General, Nation, Troop } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
||||
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
||||
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
@@ -43,7 +38,10 @@ export interface ActionContextWorldRef {
|
||||
toNationId: number;
|
||||
state: number;
|
||||
}>;
|
||||
getDiplomacyEntry(fromNationId: number, toNationId: number): {
|
||||
getDiplomacyEntry(
|
||||
fromNationId: number,
|
||||
toNationId: number
|
||||
): {
|
||||
fromNationId: number;
|
||||
toNationId: number;
|
||||
state: number;
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
||||
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
||||
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type {
|
||||
ActionContextWorldRef,
|
||||
ActionContextWorldState,
|
||||
} from './actionContext.js';
|
||||
import type { ActionContextWorldRef, ActionContextWorldState } from './actionContext.js';
|
||||
|
||||
export interface WorldSummary {
|
||||
totalGeneralCount: number;
|
||||
@@ -20,9 +17,7 @@ export interface NationSummary {
|
||||
averageDedication?: number;
|
||||
}
|
||||
|
||||
export const buildWorldSummary = (
|
||||
world: ActionContextWorldRef | null
|
||||
): WorldSummary => {
|
||||
export const buildWorldSummary = (world: ActionContextWorldRef | null): WorldSummary => {
|
||||
if (!world) {
|
||||
return { totalGeneralCount: 0, totalNpcCount: 0 };
|
||||
}
|
||||
@@ -51,16 +46,11 @@ export const buildWorldSummary = (
|
||||
};
|
||||
};
|
||||
|
||||
export const buildNationSummary = (
|
||||
world: ActionContextWorldRef | null,
|
||||
nationId: number
|
||||
): NationSummary => {
|
||||
export const buildNationSummary = (world: ActionContextWorldRef | null, nationId: number): NationSummary => {
|
||||
if (!world || nationId <= 0) {
|
||||
return {};
|
||||
}
|
||||
const generals = world.listGenerals().filter(
|
||||
(general) => general.nationId === nationId
|
||||
);
|
||||
const generals = world.listGenerals().filter((general) => general.nationId === nationId);
|
||||
if (generals.length === 0) {
|
||||
return {};
|
||||
}
|
||||
@@ -86,9 +76,7 @@ export const buildNationSummary = (
|
||||
};
|
||||
};
|
||||
|
||||
export const buildAverageNationGeneralCount = (
|
||||
world: ActionContextWorldRef | null
|
||||
): number => {
|
||||
export const buildAverageNationGeneralCount = (world: ActionContextWorldRef | null): number => {
|
||||
if (!world) {
|
||||
return 0;
|
||||
}
|
||||
@@ -100,10 +88,7 @@ export const buildAverageNationGeneralCount = (
|
||||
return generals.length / nations.length;
|
||||
};
|
||||
|
||||
export const resolveStartYear = (
|
||||
world: ActionContextWorldState,
|
||||
scenarioMeta?: ScenarioMeta
|
||||
): number => {
|
||||
export const resolveStartYear = (world: ActionContextWorldState, scenarioMeta?: ScenarioMeta): number => {
|
||||
if (typeof scenarioMeta?.startYear === 'number') {
|
||||
return scenarioMeta.startYear;
|
||||
}
|
||||
@@ -128,15 +113,9 @@ const DEFAULT_AFTER_CONFIG = {
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
|
||||
const resolveNumber = (
|
||||
record: Record<string, unknown>,
|
||||
keys: string[],
|
||||
fallback: number
|
||||
): number => {
|
||||
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
@@ -147,19 +126,14 @@ const resolveNumber = (
|
||||
};
|
||||
|
||||
// 성벽 병종은 이름/요구조건을 우선해 찾고, 없으면 기본값을 사용한다.
|
||||
const resolveCastleCrewTypeId = (
|
||||
unitSet: UnitSetDefinition,
|
||||
fallback: number
|
||||
): number => {
|
||||
const resolveCastleCrewTypeId = (unitSet: UnitSetDefinition, fallback: number): number => {
|
||||
const crewTypes = unitSet.crewTypes ?? [];
|
||||
const byName = crewTypes.find((crewType) => crewType.name.includes('성벽'));
|
||||
if (byName) {
|
||||
return byName.id;
|
||||
}
|
||||
const byRequirement = crewTypes.find((crewType) =>
|
||||
crewType.requirements.some(
|
||||
(requirement) => requirement.type === 'Impossible'
|
||||
)
|
||||
crewType.requirements.some((requirement) => requirement.type === 'Impossible')
|
||||
);
|
||||
if (byRequirement) {
|
||||
return byRequirement.id;
|
||||
@@ -170,55 +144,22 @@ const resolveCastleCrewTypeId = (
|
||||
return crewTypes[0]?.id ?? fallback;
|
||||
};
|
||||
|
||||
const resolveCastleArmType = (
|
||||
unitSet: UnitSetDefinition,
|
||||
castleCrewTypeId: number
|
||||
): number => {
|
||||
const resolveCastleArmType = (unitSet: UnitSetDefinition, castleCrewTypeId: number): number => {
|
||||
const crewTypes = unitSet.crewTypes ?? [];
|
||||
return (
|
||||
crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ??
|
||||
0
|
||||
);
|
||||
return crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ?? 0;
|
||||
};
|
||||
|
||||
export const buildWarConfig = (
|
||||
scenarioConfig: ScenarioConfig,
|
||||
unitSet: UnitSetDefinition
|
||||
): WarEngineConfig => {
|
||||
export const buildWarConfig = (scenarioConfig: ScenarioConfig, unitSet: UnitSetDefinition): WarEngineConfig => {
|
||||
const constValues = asRecord(scenarioConfig.const);
|
||||
const castleCrewTypeId = resolveNumber(
|
||||
constValues,
|
||||
['castleCrewTypeId'],
|
||||
resolveCastleCrewTypeId(unitSet, 0)
|
||||
);
|
||||
const castleCrewTypeId = resolveNumber(constValues, ['castleCrewTypeId'], resolveCastleCrewTypeId(unitSet, 0));
|
||||
const castleArmType = resolveCastleArmType(unitSet, castleCrewTypeId);
|
||||
|
||||
return {
|
||||
armPerPhase: resolveNumber(
|
||||
constValues,
|
||||
['armPerPhase', 'armperphase'],
|
||||
DEFAULT_WAR_CONFIG.armPerPhase
|
||||
),
|
||||
maxTrainByCommand: resolveNumber(
|
||||
constValues,
|
||||
['maxTrainByCommand'],
|
||||
DEFAULT_WAR_CONFIG.maxTrainByCommand
|
||||
),
|
||||
maxAtmosByCommand: resolveNumber(
|
||||
constValues,
|
||||
['maxAtmosByCommand'],
|
||||
DEFAULT_WAR_CONFIG.maxAtmosByCommand
|
||||
),
|
||||
maxTrainByWar: resolveNumber(
|
||||
constValues,
|
||||
['maxTrainByWar'],
|
||||
DEFAULT_WAR_CONFIG.maxTrainByWar
|
||||
),
|
||||
maxAtmosByWar: resolveNumber(
|
||||
constValues,
|
||||
['maxAtmosByWar'],
|
||||
DEFAULT_WAR_CONFIG.maxAtmosByWar
|
||||
),
|
||||
armPerPhase: resolveNumber(constValues, ['armPerPhase', 'armperphase'], DEFAULT_WAR_CONFIG.armPerPhase),
|
||||
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], DEFAULT_WAR_CONFIG.maxTrainByCommand),
|
||||
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
|
||||
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
|
||||
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
|
||||
castleCrewTypeId,
|
||||
armTypes: {
|
||||
footman: 1,
|
||||
@@ -238,37 +179,22 @@ export const buildWarAftermathConfig = (
|
||||
): WarAftermathConfig => {
|
||||
const constValues = asRecord(scenarioConfig.const);
|
||||
return {
|
||||
initialNationGenLimit: resolveNumber(
|
||||
constValues,
|
||||
['initialNationGenLimit'],
|
||||
0
|
||||
),
|
||||
techLevelIncYear: resolveNumber(
|
||||
constValues,
|
||||
['techLevelIncYear'],
|
||||
DEFAULT_AFTER_CONFIG.techLevelIncYear
|
||||
),
|
||||
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], 0),
|
||||
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], DEFAULT_AFTER_CONFIG.techLevelIncYear),
|
||||
initialAllowedTechLevel: resolveNumber(
|
||||
constValues,
|
||||
['initialAllowedTechLevel'],
|
||||
DEFAULT_AFTER_CONFIG.initialAllowedTechLevel
|
||||
),
|
||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||
defaultCityWall: resolveNumber(
|
||||
constValues,
|
||||
['defaultCityWall'],
|
||||
DEFAULT_AFTER_CONFIG.defaultCityWall
|
||||
),
|
||||
defaultCityWall: resolveNumber(constValues, ['defaultCityWall'], DEFAULT_AFTER_CONFIG.defaultCityWall),
|
||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||
castleCrewTypeId,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildWarTime = (
|
||||
world: ActionContextWorldState,
|
||||
scenarioMeta?: ScenarioMeta
|
||||
): WarTimeContext => ({
|
||||
export const buildWarTime = (world: ActionContextWorldState, scenarioMeta?: ScenarioMeta): WarTimeContext => ({
|
||||
year: world.currentYear,
|
||||
month: world.currentMonth,
|
||||
startYear: resolveStartYear(world, scenarioMeta),
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import {
|
||||
GENERAL_TURN_COMMAND_KEYS,
|
||||
isGeneralTurnCommandKey,
|
||||
type GeneralTurnCommandKey,
|
||||
} from './general/index.js';
|
||||
import {
|
||||
NATION_TURN_COMMAND_KEYS,
|
||||
isNationTurnCommandKey,
|
||||
type NationTurnCommandKey,
|
||||
} from './nation/index.js';
|
||||
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
|
||||
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
|
||||
|
||||
export interface TurnCommandProfile {
|
||||
general: GeneralTurnCommandKey[];
|
||||
@@ -25,9 +17,7 @@ const asStringArray = (value: unknown): string[] | null => {
|
||||
return list.length > 0 ? list : null;
|
||||
};
|
||||
|
||||
const parseKeyList = <
|
||||
T extends string
|
||||
>(options: {
|
||||
const parseKeyList = <T extends string>(options: {
|
||||
raw: unknown;
|
||||
defaults: T[];
|
||||
isKey: (value: string) => value is T;
|
||||
@@ -40,9 +30,7 @@ const parseKeyList = <
|
||||
const parsed: T[] = [];
|
||||
for (const value of rawList) {
|
||||
if (!options.isKey(value)) {
|
||||
throw new Error(
|
||||
`Unknown ${options.label} command key: ${value}`
|
||||
);
|
||||
throw new Error(`Unknown ${options.label} command key: ${value}`);
|
||||
}
|
||||
parsed.push(value);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/e
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
@@ -16,7 +13,7 @@ export interface UprisingArgs {}
|
||||
const ACTION_NAME = '거병';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, UprisingArgs> {
|
||||
public readonly key = 'che_거병';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -38,7 +35,7 @@ export class ActionDefinition<
|
||||
|
||||
// 직접 수정 (Immer Draft)
|
||||
general.meta = {
|
||||
...general.meta as object,
|
||||
...(general.meta as object),
|
||||
uprising: true as TriggerValue,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/e
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
@@ -16,7 +13,7 @@ export interface FoundingArgs {}
|
||||
const ACTION_NAME = '건국';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, FoundingArgs> {
|
||||
public readonly key = 'che_건국';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -38,7 +35,7 @@ export class ActionDefinition<
|
||||
|
||||
// 직접 수정 (Immer Draft)
|
||||
general.meta = {
|
||||
...general.meta as object,
|
||||
...(general.meta as object),
|
||||
founding: true as TriggerValue,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -47,34 +39,22 @@ const SIGHTSEEING_MESSAGES: Array<{
|
||||
},
|
||||
{
|
||||
flags: IncHeavyExp,
|
||||
texts: [
|
||||
'주점에서 사람들과 어울려 술을 마셨습니다.',
|
||||
'위기에 빠진 사람을 구해주었습니다.',
|
||||
],
|
||||
texts: ['주점에서 사람들과 어울려 술을 마셨습니다.', '위기에 빠진 사람을 구해주었습니다.'],
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
flags: IncHeavyExp | IncLeadership,
|
||||
texts: [
|
||||
'백성들에게 현인의 가르침을 설파했습니다.',
|
||||
'어느 집의 도망친 가축을 되찾아 주었습니다.',
|
||||
],
|
||||
texts: ['백성들에게 현인의 가르침을 설파했습니다.', '어느 집의 도망친 가축을 되찾아 주었습니다.'],
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
flags: IncHeavyExp | IncStrength,
|
||||
texts: [
|
||||
'동네 장사와 힘겨루기를 하여 멋지게 이겼습니다.',
|
||||
'어느 집의 무너진 울타리를 고쳐주었습니다.',
|
||||
],
|
||||
texts: ['동네 장사와 힘겨루기를 하여 멋지게 이겼습니다.', '어느 집의 무너진 울타리를 고쳐주었습니다.'],
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
flags: IncHeavyExp | IncIntel,
|
||||
texts: [
|
||||
'어느 명사와 설전을 벌여 멋지게 이겼습니다.',
|
||||
'거리에서 글 모르는 아이들을 모아 글을 가르쳤습니다.',
|
||||
],
|
||||
texts: ['어느 명사와 설전을 벌여 멋지게 이겼습니다.', '거리에서 글 모르는 아이들을 모아 글을 가르쳤습니다.'],
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
@@ -89,10 +69,7 @@ const SIGHTSEEING_MESSAGES: Array<{
|
||||
},
|
||||
{
|
||||
flags: IncExp | DecGold,
|
||||
texts: [
|
||||
'산적을 만나 금 :goldAmount:을 빼앗겼습니다.',
|
||||
'돈을 :goldAmount: 빌려주었다가 떼어먹혔습니다.',
|
||||
],
|
||||
texts: ['산적을 만나 금 :goldAmount:을 빼앗겼습니다.', '돈을 :goldAmount: 빌려주었다가 떼어먹혔습니다.'],
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
@@ -127,10 +104,7 @@ const SIGHTSEEING_MESSAGES: Array<{
|
||||
},
|
||||
{
|
||||
flags: IncHeavyExp | IncStrength | IncRice,
|
||||
texts: [
|
||||
'호랑이를 잡아 고기 :riceAmount:을 얻었습니다.',
|
||||
'곰을 잡아 고기 :riceAmount:을 얻었습니다.',
|
||||
],
|
||||
texts: ['호랑이를 잡아 고기 :riceAmount:을 얻었습니다.', '곰을 잡아 고기 :riceAmount:을 얻었습니다.'],
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
@@ -145,16 +119,11 @@ const SIGHTSEEING_MESSAGES: Array<{
|
||||
},
|
||||
];
|
||||
|
||||
const pickByWeight = (
|
||||
rng: GeneralActionResolveContext['rng']
|
||||
): { flags: number; text: string } => {
|
||||
const pickByWeight = (rng: GeneralActionResolveContext['rng']): { flags: number; text: string } => {
|
||||
if (SIGHTSEEING_MESSAGES.length === 0) {
|
||||
return { flags: 0, text: '' };
|
||||
}
|
||||
const total = SIGHTSEEING_MESSAGES.reduce(
|
||||
(sum, entry) => sum + Math.max(entry.weight, 0),
|
||||
0
|
||||
);
|
||||
const total = SIGHTSEEING_MESSAGES.reduce((sum, entry) => sum + Math.max(entry.weight, 0), 0);
|
||||
const base = SIGHTSEEING_MESSAGES[0];
|
||||
if (!base) {
|
||||
return { flags: 0, text: '' };
|
||||
@@ -183,7 +152,7 @@ const pickByWeight = (
|
||||
};
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, SightseeingArgs> {
|
||||
public readonly key = 'che_견문';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -193,10 +162,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: SightseeingArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: SightseeingArgs): Constraint[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
notWanderingNation,
|
||||
@@ -15,10 +8,7 @@ import {
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -43,7 +33,7 @@ const readTech = (nation: Nation | null | undefined): number => {
|
||||
};
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, TechResearchArgs> {
|
||||
public readonly key = 'che_기술연구';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -58,19 +48,9 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: TechResearchArgs
|
||||
): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.costGold ?? 0;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
notWanderingNation(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
reqGeneralGold(getRequiredGold),
|
||||
];
|
||||
buildConstraints(_ctx: ConstraintContext, _args: TechResearchArgs): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
|
||||
return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(getRequiredGold)];
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -87,8 +67,7 @@ export class ActionDefinition<
|
||||
const delta = this.env.techDelta ?? DEFAULT_TECH_DELTA;
|
||||
const currentTech = readTech(nation);
|
||||
const maxTech =
|
||||
typeof this.env.maxTechLevel === 'number' &&
|
||||
this.env.maxTechLevel > 0
|
||||
typeof this.env.maxTechLevel === 'number' && this.env.maxTechLevel > 0
|
||||
? this.env.maxTechLevel
|
||||
: currentTech + delta;
|
||||
const nextTech = Math.min(currentTech + delta, maxTech);
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -40,7 +32,7 @@ const reqDomesticSpecial = (): Constraint => ({
|
||||
});
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, ResetSpecialDomesticArgs> {
|
||||
public readonly key = 'che_내정특기초기화';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -50,10 +42,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: ResetSpecialDomesticArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialDomesticArgs): Constraint[] {
|
||||
return [reqDomesticSpecial()];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||
super(
|
||||
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition({ develCost: env.develCost }),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
|
||||
};
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
reqGeneralCrew,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { notBeNeutral, reqGeneralCrew, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type {
|
||||
ActionContextBuilder,
|
||||
} from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.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 { GeneralTurnCommandSpec } from './index.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
@@ -30,7 +14,7 @@ import { getMetaNumber, setMetaNumber, increaseMetaNumber } from '@sammo-ts/logi
|
||||
export interface DrillArgs {}
|
||||
|
||||
export interface DrillContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
unitSet?: UnitSetDefinition | null;
|
||||
}
|
||||
@@ -45,16 +29,10 @@ type DrillPick = 'success' | 'normal' | 'fail';
|
||||
|
||||
const ACTION_NAME = '단련';
|
||||
|
||||
const resolveArmTypeName = (
|
||||
unitSet: UnitSetDefinition,
|
||||
armType: number
|
||||
): string =>
|
||||
const resolveArmTypeName = (unitSet: UnitSetDefinition, armType: number): string =>
|
||||
unitSet.armTypes?.[String(armType)] ?? `병종${armType}`;
|
||||
|
||||
const pickByWeight = <T extends string>(
|
||||
rng: DrillContext['rng'],
|
||||
weights: Record<T, number>
|
||||
): T => {
|
||||
const pickByWeight = <T extends string>(rng: DrillContext['rng'], weights: Record<T, number>): T => {
|
||||
const entries = Object.entries(weights) as Array<[T, number]>;
|
||||
const first = entries[0];
|
||||
if (!first) {
|
||||
@@ -83,11 +61,7 @@ const pickByWeight = <T extends string>(
|
||||
return last ? last[0] : first[0];
|
||||
};
|
||||
|
||||
const reqGeneralStat = (
|
||||
key: 'train' | 'atmos',
|
||||
label: string,
|
||||
minValue: number
|
||||
): Constraint => ({
|
||||
const reqGeneralStat = (key: 'train' | 'atmos', label: string, minValue: number): Constraint => ({
|
||||
name: `ReqGeneral${label}`,
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||
test: (ctx, view) => {
|
||||
@@ -105,12 +79,8 @@ const reqGeneralStat = (
|
||||
});
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DrillArgs,
|
||||
DrillContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DrillArgs, DrillContext<TriggerState>> {
|
||||
public readonly key = 'che_단련';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly env: DrillEnvironment;
|
||||
@@ -124,16 +94,11 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DrillArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DrillArgs): Constraint[] {
|
||||
const trainLow = this.env.defaultTrainLow ?? 40;
|
||||
const atmosLow = this.env.defaultAtmosLow ?? 40;
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.develCost ?? 0;
|
||||
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.develCost ?? 0;
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
|
||||
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
reqGeneralCrew(),
|
||||
@@ -144,19 +109,14 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DrillContext<TriggerState>,
|
||||
_args: DrillArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: DrillContext<TriggerState>, _args: DrillArgs): GeneralActionOutcome<TriggerState> {
|
||||
if (!context.unitSet) {
|
||||
context.addLog('병종 정보를 확인할 수 없어 단련을 진행할 수 없습니다.');
|
||||
return { effects: [] };
|
||||
}
|
||||
|
||||
const general = context.general;
|
||||
const crewType = context.unitSet.crewTypes?.find(
|
||||
(entry) => entry.id === general.crewTypeId
|
||||
);
|
||||
const crewType = context.unitSet.crewTypes?.find((entry) => entry.id === general.crewTypeId);
|
||||
if (!crewType) {
|
||||
context.addLog('병종 정보를 확인할 수 없어 단련을 진행할 수 없습니다.');
|
||||
return { effects: [] };
|
||||
@@ -169,20 +129,10 @@ export class ActionDefinition<
|
||||
});
|
||||
const multiplier = pick === 'success' ? 3 : pick === 'normal' ? 2 : 1;
|
||||
|
||||
const baseScore = Math.round(
|
||||
(general.crew * general.train * general.atmos) / 20 / 10000
|
||||
);
|
||||
const baseScore = Math.round((general.crew * general.train * general.atmos) / 20 / 10000);
|
||||
const score = baseScore * multiplier;
|
||||
const armTypeName = resolveArmTypeName(
|
||||
context.unitSet,
|
||||
crewType.armType
|
||||
);
|
||||
const logPrefix =
|
||||
pick === 'success'
|
||||
? '단련이 일취월장하여'
|
||||
: pick === 'fail'
|
||||
? '단련이 지지부진하여'
|
||||
: '';
|
||||
const armTypeName = resolveArmTypeName(context.unitSet, crewType.armType);
|
||||
const logPrefix = pick === 'success' ? '단련이 일취월장하여' : pick === 'fail' ? '단련이 지지부진하여' : '';
|
||||
const logText = logPrefix
|
||||
? `${logPrefix} ${armTypeName} 숙련도가 ${score} 향상되었습니다.`
|
||||
: `${armTypeName} 숙련도가 ${score} 향상되었습니다.`;
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -30,7 +21,7 @@ const DEFAULT_ATMOS_DELTA = 5;
|
||||
const DEFAULT_MAX_ATMOS = 100;
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, BoostMoraleArgs> {
|
||||
public readonly key = 'che_사기진작';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -45,12 +36,8 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: BoostMoraleArgs
|
||||
): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.costGold ?? 0;
|
||||
buildConstraints(_ctx: ConstraintContext, _args: BoostMoraleArgs): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
|
||||
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
|
||||
}
|
||||
|
||||
@@ -63,10 +50,7 @@ export class ActionDefinition<
|
||||
this.env.maxAtmosByCommand && this.env.maxAtmosByCommand > 0
|
||||
? this.env.maxAtmosByCommand
|
||||
: DEFAULT_MAX_ATMOS;
|
||||
const delta =
|
||||
this.env.atmosDelta && this.env.atmosDelta > 0
|
||||
? this.env.atmosDelta
|
||||
: DEFAULT_ATMOS_DELTA;
|
||||
const delta = this.env.atmosDelta && this.env.atmosDelta > 0 ? this.env.atmosDelta : DEFAULT_ATMOS_DELTA;
|
||||
const nextAtmos = clamp(general.atmos + delta, 0, maxAtmos);
|
||||
const applied = nextAtmos - general.atmos;
|
||||
const costGold = this.env.costGold ?? 0;
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
notWanderingNation,
|
||||
@@ -21,10 +11,7 @@ import {
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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 {
|
||||
GeneralActionOutcome,
|
||||
@@ -39,7 +26,7 @@ import { clamp } from 'es-toolkit';
|
||||
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
||||
|
||||
export interface DomesticActionContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionContext<TriggerState> {
|
||||
general: General<TriggerState>;
|
||||
city: City;
|
||||
@@ -52,14 +39,8 @@ export interface InvestmentEnvironment {
|
||||
frontDebuff?: number;
|
||||
frontStatesWithDebuff?: number[];
|
||||
getDomesticExpLevelBonus?: (expLevel: number) => number;
|
||||
getCriticalRatio?: (
|
||||
context: DomesticActionContext,
|
||||
statKey: string
|
||||
) => { success: number; fail: number };
|
||||
getCriticalScoreMultiplier?: (
|
||||
rng: RandomGenerator,
|
||||
pick: DomesticCriticalPick
|
||||
) => number;
|
||||
getCriticalRatio?: (context: DomesticActionContext, statKey: string) => { success: number; fail: number };
|
||||
getCriticalScoreMultiplier?: (rng: RandomGenerator, pick: DomesticCriticalPick) => number;
|
||||
adjustFrontDebuff?: (context: DomesticActionContext, debuff: number) => number;
|
||||
}
|
||||
|
||||
@@ -82,23 +63,15 @@ const ACTION_NAME = '상업 투자';
|
||||
const CITY_KEY = 'commerce';
|
||||
const STAT_EXP_KEY = 'intel_exp';
|
||||
|
||||
const getMetaNumber = (
|
||||
meta: Record<string, unknown>,
|
||||
key: string
|
||||
): number | null => {
|
||||
const getMetaNumber = (meta: Record<string, unknown>, key: string): number | null => {
|
||||
const raw = meta[key];
|
||||
return typeof raw === 'number' ? raw : null;
|
||||
};
|
||||
|
||||
const randomRange = (rng: RandomGenerator, min: number, max: number): number =>
|
||||
min + (max - min) * rng.nextFloat();
|
||||
const randomRange = (rng: RandomGenerator, min: number, max: number): number => min + (max - min) * rng.nextFloat();
|
||||
|
||||
const pickByWeight = (
|
||||
rng: RandomGenerator,
|
||||
weights: Record<DomesticCriticalPick, number>
|
||||
): DomesticCriticalPick => {
|
||||
const total =
|
||||
weights.fail + weights.normal + weights.success;
|
||||
const pickByWeight = (rng: RandomGenerator, weights: Record<DomesticCriticalPick, number>): DomesticCriticalPick => {
|
||||
const total = weights.fail + weights.normal + weights.success;
|
||||
if (total <= 0) {
|
||||
return 'normal';
|
||||
}
|
||||
@@ -112,18 +85,12 @@ const pickByWeight = (
|
||||
return 'normal';
|
||||
};
|
||||
|
||||
const addMetaNumber = (
|
||||
meta: Record<string, unknown>,
|
||||
key: string,
|
||||
delta: number
|
||||
): Record<string, unknown> => {
|
||||
const addMetaNumber = (meta: Record<string, unknown>, key: string, delta: number): Record<string, unknown> => {
|
||||
const current = getMetaNumber(meta, key) ?? 0;
|
||||
return { ...meta, [key]: current + delta };
|
||||
};
|
||||
|
||||
const buildDomesticContextFromView = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
const buildDomesticContextFromView = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): DomesticActionContext<TriggerState> | null => {
|
||||
@@ -141,10 +108,7 @@ const buildDomesticContextFromView = <
|
||||
}
|
||||
const nationId = ctx.nationId ?? general.nationId;
|
||||
const nation =
|
||||
nationId !== undefined
|
||||
? ((view.get({ kind: 'nation', id: nationId }) as Nation | null) ??
|
||||
null)
|
||||
: null;
|
||||
nationId !== undefined ? ((view.get({ kind: 'nation', id: nationId }) as Nation | null) ?? null) : null;
|
||||
|
||||
return {
|
||||
general,
|
||||
@@ -154,18 +118,13 @@ const buildDomesticContextFromView = <
|
||||
};
|
||||
|
||||
// 상업 투자 결과치를 계산하는 경로를 제공한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
private readonly env: InvestmentEnvironment;
|
||||
private readonly actionKey = '상업';
|
||||
private readonly statKey = 'intelligence';
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: InvestmentEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: InvestmentEnvironment) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
this.env = env;
|
||||
}
|
||||
@@ -175,84 +134,42 @@ export class CommandResolver<
|
||||
rice: number;
|
||||
} {
|
||||
const baseGold = this.env.develCost;
|
||||
const gold = Math.round(
|
||||
this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
this.actionKey,
|
||||
'cost',
|
||||
baseGold
|
||||
)
|
||||
);
|
||||
const gold = Math.round(this.pipeline.onCalcDomestic(context, this.actionKey, 'cost', baseGold));
|
||||
return { gold, rice: 0 };
|
||||
}
|
||||
|
||||
calcBaseScore(
|
||||
context: DomesticActionContext<TriggerState>,
|
||||
rng: RandomGenerator
|
||||
): number {
|
||||
const trust =
|
||||
getMetaNumber(context.city.meta, 'trust') ??
|
||||
this.env.defaultTrust ??
|
||||
DEFAULT_TRUST;
|
||||
calcBaseScore(context: DomesticActionContext<TriggerState>, rng: RandomGenerator): number {
|
||||
const trust = getMetaNumber(context.city.meta, 'trust') ?? this.env.defaultTrust ?? DEFAULT_TRUST;
|
||||
|
||||
let score = this.pipeline.onCalcStat(
|
||||
context,
|
||||
this.statKey,
|
||||
context.general.stats.intelligence
|
||||
);
|
||||
let score = this.pipeline.onCalcStat(context, this.statKey, context.general.stats.intelligence);
|
||||
|
||||
const expLevel =
|
||||
getMetaNumber(context.general.meta, 'explevel') ??
|
||||
getMetaNumber(context.general.meta, 'expLevel') ??
|
||||
0;
|
||||
const expBonus =
|
||||
this.env.getDomesticExpLevelBonus?.(expLevel) ?? 1;
|
||||
getMetaNumber(context.general.meta, 'explevel') ?? getMetaNumber(context.general.meta, 'expLevel') ?? 0;
|
||||
const expBonus = this.env.getDomesticExpLevelBonus?.(expLevel) ?? 1;
|
||||
|
||||
score *= trust / 100;
|
||||
score *= expBonus;
|
||||
score *= randomRange(rng, 0.8, 1.2);
|
||||
|
||||
return this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
this.actionKey,
|
||||
'score',
|
||||
score
|
||||
);
|
||||
return this.pipeline.onCalcDomestic(context, this.actionKey, 'score', score);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DomesticActionContext<TriggerState>,
|
||||
rng: RandomGenerator
|
||||
): CommerceInvestmentResult {
|
||||
resolve(context: DomesticActionContext<TriggerState>, rng: RandomGenerator): CommerceInvestmentResult {
|
||||
const { gold: costGold, rice: costRice } = this.getCost(context);
|
||||
const trust =
|
||||
getMetaNumber(context.city.meta, 'trust') ??
|
||||
this.env.defaultTrust ??
|
||||
DEFAULT_TRUST;
|
||||
const trust = getMetaNumber(context.city.meta, 'trust') ?? this.env.defaultTrust ?? DEFAULT_TRUST;
|
||||
let score = clamp(this.calcBaseScore(context, rng), 1, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const ratio =
|
||||
this.env.getCriticalRatio?.(context, this.statKey) ?? {
|
||||
success: 0,
|
||||
fail: 0,
|
||||
};
|
||||
const ratio = this.env.getCriticalRatio?.(context, this.statKey) ?? {
|
||||
success: 0,
|
||||
fail: 0,
|
||||
};
|
||||
let successRatio = ratio.success;
|
||||
let failRatio = ratio.fail;
|
||||
if (trust < 80) {
|
||||
successRatio *= trust / 80;
|
||||
}
|
||||
successRatio = this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
this.actionKey,
|
||||
'success',
|
||||
successRatio
|
||||
);
|
||||
failRatio = this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
this.actionKey,
|
||||
'fail',
|
||||
failRatio
|
||||
);
|
||||
successRatio = this.pipeline.onCalcDomestic(context, this.actionKey, 'success', successRatio);
|
||||
failRatio = this.pipeline.onCalcDomestic(context, this.actionKey, 'fail', failRatio);
|
||||
|
||||
successRatio = clamp(successRatio, 0, 1);
|
||||
failRatio = clamp(failRatio, 0, 1 - successRatio);
|
||||
@@ -264,18 +181,14 @@ export class CommandResolver<
|
||||
normal: normalRatio,
|
||||
});
|
||||
|
||||
const criticalMultiplier =
|
||||
this.env.getCriticalScoreMultiplier?.(rng, pick) ?? 1;
|
||||
const criticalMultiplier = this.env.getCriticalScoreMultiplier?.(rng, pick) ?? 1;
|
||||
score = Math.round(score * criticalMultiplier);
|
||||
|
||||
const frontStates =
|
||||
this.env.frontStatesWithDebuff ?? DEFAULT_FRONT_STATES;
|
||||
const frontStates = this.env.frontStatesWithDebuff ?? DEFAULT_FRONT_STATES;
|
||||
let appliedFrontDebuff = false;
|
||||
if (frontStates.includes(context.city.frontState)) {
|
||||
const baseDebuff =
|
||||
this.env.frontDebuff ?? DEFAULT_FRONT_DEBUFF;
|
||||
const adjustedDebuff =
|
||||
this.env.adjustFrontDebuff?.(context, baseDebuff) ?? baseDebuff;
|
||||
const baseDebuff = this.env.frontDebuff ?? DEFAULT_FRONT_DEBUFF;
|
||||
const adjustedDebuff = this.env.adjustFrontDebuff?.(context, baseDebuff) ?? baseDebuff;
|
||||
score *= adjustedDebuff;
|
||||
appliedFrontDebuff = true;
|
||||
}
|
||||
@@ -296,15 +209,12 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, CommerceInvestmentArgs> {
|
||||
readonly key = 'che_상업투자';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: InvestmentEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: InvestmentEnvironment) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
}
|
||||
|
||||
@@ -329,11 +239,7 @@ export class ActionResolver<
|
||||
);
|
||||
|
||||
// 직접 수정 (Immer Draft)
|
||||
city.commerce = clamp(
|
||||
city.commerce + result.score,
|
||||
0,
|
||||
city.commerceMax
|
||||
);
|
||||
city.commerce = clamp(city.commerce + result.score, 0, city.commerceMax);
|
||||
|
||||
general.gold = Math.max(0, general.gold - result.costGold);
|
||||
general.rice = Math.max(0, general.rice - result.costRice);
|
||||
@@ -346,12 +252,7 @@ export class ActionResolver<
|
||||
? { ...metaWithStatExp, max_domestic_critical: result.score }
|
||||
: { ...metaWithStatExp, max_domestic_critical: 0 };
|
||||
|
||||
const pickLabel =
|
||||
result.pick === 'success'
|
||||
? '성공'
|
||||
: result.pick === 'fail'
|
||||
? '실패'
|
||||
: '완료';
|
||||
const pickLabel = result.pick === 'success' ? '성공' : result.pick === 'fail' ? '실패' : '완료';
|
||||
const logMessage = `${ACTION_NAME} ${pickLabel}: +${Math.round(result.score)}`;
|
||||
context.addLog(logMessage);
|
||||
|
||||
@@ -360,17 +261,14 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, CommerceInvestmentArgs> {
|
||||
public readonly key = 'che_상업투자';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: InvestmentEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: InvestmentEnvironment) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
this.resolver = new ActionResolver(modules, env);
|
||||
}
|
||||
@@ -380,10 +278,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
_args: CommerceInvestmentArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(ctx: ConstraintContext, _args: CommerceInvestmentArgs): Constraint[] {
|
||||
void _args;
|
||||
const requirements: RequirementKey[] = [];
|
||||
if (ctx.cityId !== undefined) {
|
||||
@@ -394,8 +289,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
const getCost = (context: ConstraintContext, view: StateView): number => {
|
||||
const domesticContext =
|
||||
buildDomesticContextFromView<TriggerState>(context, view);
|
||||
const domesticContext = buildDomesticContextFromView<TriggerState>(context, view);
|
||||
if (!domesticContext) {
|
||||
return 0;
|
||||
}
|
||||
@@ -429,6 +323,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||
super(
|
||||
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition({ develCost: env.develCost }),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||
super(
|
||||
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition({ develCost: env.develCost }),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
|
||||
};
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { notBeNeutral, occupiedCity, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type {
|
||||
ActionContextBuilder,
|
||||
} from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.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 { GeneralTurnCommandSpec } from './index.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
@@ -32,7 +16,7 @@ export interface DexTransferArgs {
|
||||
}
|
||||
|
||||
export interface DexTransferContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
unitSet?: UnitSetDefinition | null;
|
||||
}
|
||||
@@ -55,19 +39,12 @@ const resolveArmType = (value: unknown): number | null => {
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const resolveArmTypeName = (
|
||||
unitSet: UnitSetDefinition | null | undefined,
|
||||
armType: number
|
||||
): string =>
|
||||
const resolveArmTypeName = (unitSet: UnitSetDefinition | null | undefined, armType: number): string =>
|
||||
unitSet?.armTypes?.[String(armType)] ?? `병종${armType}`;
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DexTransferArgs,
|
||||
DexTransferContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DexTransferArgs, DexTransferContext<TriggerState>> {
|
||||
public readonly key = 'che_숙련전환';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly env: DexTransferEnvironment;
|
||||
@@ -89,26 +66,13 @@ export class ActionDefinition<
|
||||
return { srcArmType, destArmType };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DexTransferArgs
|
||||
): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.develCost ?? 0;
|
||||
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.develCost ?? 0;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
reqGeneralGold(getRequiredGold),
|
||||
reqGeneralRice(getRequiredRice),
|
||||
];
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DexTransferArgs): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
|
||||
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
|
||||
return [notBeNeutral(), occupiedCity(), reqGeneralGold(getRequiredGold), reqGeneralRice(getRequiredRice)];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DexTransferContext<TriggerState>,
|
||||
args: DexTransferArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: DexTransferContext<TriggerState>, args: DexTransferArgs): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
const srcKey = `dex${args.srcArmType}`;
|
||||
const destKey = `dex${args.destArmType}`;
|
||||
@@ -117,20 +81,10 @@ export class ActionDefinition<
|
||||
const addDex = Math.trunc(cutDex * CONVERT_COEFF);
|
||||
|
||||
setMetaNumber(general.meta, srcKey, srcDex - cutDex);
|
||||
setMetaNumber(
|
||||
general.meta,
|
||||
destKey,
|
||||
getMetaNumber(general.meta, destKey, 0) + addDex
|
||||
);
|
||||
setMetaNumber(general.meta, destKey, getMetaNumber(general.meta, destKey, 0) + addDex);
|
||||
|
||||
const srcName = resolveArmTypeName(
|
||||
context.unitSet,
|
||||
args.srcArmType
|
||||
);
|
||||
const destName = resolveArmTypeName(
|
||||
context.unitSet,
|
||||
args.destArmType
|
||||
);
|
||||
const srcName = resolveArmTypeName(context.unitSet, args.srcArmType);
|
||||
const destName = resolveArmTypeName(context.unitSet, args.destArmType);
|
||||
const cutJosa = JosaUtil.pick(String(cutDex), '을');
|
||||
const addJosa = JosaUtil.pick(String(addDex), '으로');
|
||||
|
||||
@@ -140,9 +94,7 @@ export class ActionDefinition<
|
||||
general.experience += 10;
|
||||
increaseMetaNumber(general.meta, 'leadership_exp', 2);
|
||||
|
||||
context.addLog(
|
||||
`${srcName} 숙련 ${cutDex}${cutJosa} ${destName} 숙련 ${addDex}${addJosa} 전환했습니다.`
|
||||
);
|
||||
context.addLog(`${srcName} 숙련 ${cutDex}${cutJosa} ${destName} 숙련 ${addDex}${addJosa} 전환했습니다.`);
|
||||
|
||||
return { effects: [] };
|
||||
}
|
||||
@@ -158,6 +110,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '군사',
|
||||
reqArg: true,
|
||||
args: { srcArmType: 0, destArmType: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition({ develCost: env.develCost }),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
|
||||
};
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -27,7 +18,7 @@ const ACTION_NAME = '요양';
|
||||
const DEFAULT_INJURY_DELTA = 10;
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, RecoveryArgs> {
|
||||
public readonly key = 'che_요양';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -42,12 +33,8 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: RecoveryArgs
|
||||
): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.costGold ?? 0;
|
||||
buildConstraints(_ctx: ConstraintContext, _args: RecoveryArgs): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
|
||||
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
StatBlock,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { City, General, GeneralTriggerState, StatBlock, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { reqGeneralGold, reqGeneralRice } 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 {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import {
|
||||
createGeneralAddEffect,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createGeneralAddEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import { buildRecruitmentGeneral } from './recruitment.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
@@ -55,7 +38,7 @@ export interface TalentScoutWorldSummary {
|
||||
}
|
||||
|
||||
export interface TalentScoutResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
currentYear: number;
|
||||
worldSummary: TalentScoutWorldSummary;
|
||||
@@ -77,14 +60,8 @@ export interface TalentScoutEnvironment {
|
||||
minDeathYears?: number;
|
||||
maxDeathYears?: number;
|
||||
decorateName?: (name: string, npcState: number) => string;
|
||||
pickCandidate?: (
|
||||
context: TalentScoutResolveContext,
|
||||
rng: RandomGenerator
|
||||
) => TalentScoutCandidate | null;
|
||||
pickSpawnCityId?: (
|
||||
context: TalentScoutResolveContext,
|
||||
rng: RandomGenerator
|
||||
) => number | null;
|
||||
pickCandidate?: (context: TalentScoutResolveContext, rng: RandomGenerator) => TalentScoutCandidate | null;
|
||||
pickSpawnCityId?: (context: TalentScoutResolveContext, rng: RandomGenerator) => number | null;
|
||||
buildStats?: (
|
||||
context: TalentScoutResolveContext,
|
||||
rng: RandomGenerator,
|
||||
@@ -118,15 +95,11 @@ const addMetaNumber = (
|
||||
key: StatExpKey,
|
||||
delta: number
|
||||
): Record<string, TriggerValue> => {
|
||||
const current =
|
||||
typeof meta[key] === 'number' ? (meta[key] as number) : 0;
|
||||
const current = typeof meta[key] === 'number' ? (meta[key] as number) : 0;
|
||||
return { ...meta, [key]: current + delta };
|
||||
};
|
||||
|
||||
const pickByWeight = <T extends string>(
|
||||
rng: RandomGenerator,
|
||||
weights: Record<T, number>
|
||||
): T => {
|
||||
const pickByWeight = <T extends string>(rng: RandomGenerator, weights: Record<T, number>): T => {
|
||||
const entries = Object.entries(weights) as Array<[T, number]>;
|
||||
const first = entries[0];
|
||||
if (!first) {
|
||||
@@ -155,26 +128,18 @@ const pickByWeight = <T extends string>(
|
||||
return last ? last[0] : first[0];
|
||||
};
|
||||
|
||||
const pickStatExpKey = (
|
||||
rng: RandomGenerator,
|
||||
general: General
|
||||
): StatExpKey =>
|
||||
const pickStatExpKey = (rng: RandomGenerator, general: General): StatExpKey =>
|
||||
pickByWeight(rng, {
|
||||
leadership_exp: general.stats.leadership,
|
||||
strength_exp: general.stats.strength,
|
||||
intel_exp: general.stats.intelligence,
|
||||
});
|
||||
|
||||
const calcFoundProp = (
|
||||
maxGeneral: number,
|
||||
totalGeneralCount: number,
|
||||
totalNpcCount: number
|
||||
): number => {
|
||||
const calcFoundProp = (maxGeneral: number, totalGeneralCount: number, totalNpcCount: number): number => {
|
||||
if (maxGeneral <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const current =
|
||||
totalGeneralCount + totalNpcCount / 2;
|
||||
const current = totalGeneralCount + totalNpcCount / 2;
|
||||
const remainSlot = Math.max(maxGeneral - current, 0);
|
||||
const main = Math.pow(remainSlot / maxGeneral, 6);
|
||||
const small = 1 / (totalNpcCount / 3 + 1);
|
||||
@@ -185,11 +150,7 @@ const calcFoundProp = (
|
||||
return Math.max(main, big);
|
||||
};
|
||||
|
||||
const randomRangeInt = (
|
||||
rng: RandomGenerator,
|
||||
min: number,
|
||||
max: number
|
||||
): number => rng.nextInt(min, max + 1);
|
||||
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
|
||||
|
||||
const resolveCandidate = (
|
||||
context: TalentScoutResolveContext,
|
||||
@@ -235,8 +196,7 @@ const resolveStats = (
|
||||
if (env.buildStats) {
|
||||
return env.buildStats(context, rng, candidate);
|
||||
}
|
||||
const fallback =
|
||||
context.worldSummary.averageStats ?? context.general.stats;
|
||||
const fallback = context.worldSummary.averageStats ?? context.general.stats;
|
||||
return {
|
||||
leadership: candidate.stats?.leadership ?? fallback.leadership,
|
||||
strength: candidate.stats?.strength ?? fallback.strength,
|
||||
@@ -245,16 +205,11 @@ const resolveStats = (
|
||||
};
|
||||
|
||||
// 인재탐색 확률과 비용을 계산한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
private readonly env: TalentScoutEnvironment;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: TalentScoutEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
this.env = env;
|
||||
}
|
||||
@@ -272,27 +227,19 @@ export class CommandResolver<
|
||||
context.worldSummary.totalGeneralCount,
|
||||
context.worldSummary.totalNpcCount
|
||||
);
|
||||
return this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
ACTION_KEY,
|
||||
'probability',
|
||||
base
|
||||
);
|
||||
return this.pipeline.onCalcDomestic(context, ACTION_KEY, 'probability', base);
|
||||
}
|
||||
}
|
||||
|
||||
// 인재탐색 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, TalentScoutArgs> {
|
||||
readonly key = 'che_인재탐색';
|
||||
private readonly env: TalentScoutEnvironment;
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: TalentScoutEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
|
||||
this.env = env;
|
||||
this.command = new CommandResolver(modules, env);
|
||||
}
|
||||
@@ -308,10 +255,7 @@ export class ActionResolver<
|
||||
const found = context.rng.nextBool(prop);
|
||||
|
||||
const statKey = pickStatExpKey(context.rng, general);
|
||||
const metaAfter =
|
||||
found
|
||||
? addMetaNumber(general.meta, statKey, 3)
|
||||
: addMetaNumber(general.meta, statKey, 1);
|
||||
const metaAfter = found ? addMetaNumber(general.meta, statKey, 3) : addMetaNumber(general.meta, statKey, 1);
|
||||
|
||||
const nextGold = Math.max(0, general.gold - reqGold);
|
||||
const nextRice = Math.max(0, general.rice - reqRice);
|
||||
@@ -335,8 +279,7 @@ export class ActionResolver<
|
||||
|
||||
const candidate = resolveCandidate(context, context.rng, this.env);
|
||||
const newGeneralId = context.createGeneralId();
|
||||
const resolvedCandidate: TalentScoutCandidate =
|
||||
candidate ?? { name: `NPC_${newGeneralId}` };
|
||||
const resolvedCandidate: TalentScoutCandidate = candidate ?? { name: `NPC_${newGeneralId}` };
|
||||
|
||||
const age = randomRangeInt(
|
||||
context.rng,
|
||||
@@ -351,12 +294,7 @@ export class ActionResolver<
|
||||
this.env.minDeathYears ?? DEFAULT_DEATH_MIN,
|
||||
this.env.maxDeathYears ?? DEFAULT_DEATH_MAX
|
||||
);
|
||||
const stats = resolveStats(
|
||||
context,
|
||||
context.rng,
|
||||
this.env,
|
||||
resolvedCandidate
|
||||
);
|
||||
const stats = resolveStats(context, context.rng, this.env, resolvedCandidate);
|
||||
const name = this.env.decorateName
|
||||
? this.env.decorateName(resolvedCandidate.name, NPC_TYPE)
|
||||
: resolvedCandidate.name;
|
||||
@@ -415,21 +353,14 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
TalentScoutArgs,
|
||||
TalentScoutResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, TalentScoutArgs, TalentScoutResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_인재탐색';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: TalentScoutEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
this.resolver = new ActionResolver(modules, env);
|
||||
}
|
||||
@@ -439,17 +370,11 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: TalentScoutArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: TalentScoutArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
const { gold, rice } = this.command.getCost();
|
||||
return [
|
||||
reqGeneralGold(() => gold),
|
||||
reqGeneralRice(() => rice),
|
||||
];
|
||||
return [reqGeneralGold(() => gold), reqGeneralRice(() => rice)];
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -473,6 +398,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '인사',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
};
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { GeneralTriggerState } 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 { beNeutral, existsDestNation } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
@@ -28,7 +22,7 @@ const parseNationId = (raw: unknown): number | null => {
|
||||
};
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, AppointmentArgs> {
|
||||
public readonly key = 'che_임관';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -50,13 +44,10 @@ export class ActionDefinition<
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
args: AppointmentArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
context.addLog(
|
||||
`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
);
|
||||
context.addLog(`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`, {
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
return { effects: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -40,7 +32,7 @@ const reqWarSpecial = (): Constraint => ({
|
||||
});
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, ResetSpecialWarArgs> {
|
||||
public readonly key = 'che_전투특기초기화';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -50,10 +42,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: ResetSpecialWarArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialWarArgs): Constraint[] {
|
||||
return [reqWarSpecial()];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||
super(
|
||||
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition({ develCost: env.develCost }),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { General, GeneralTriggerState, Troop } 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 {
|
||||
mustBeTroopLeader,
|
||||
notBeNeutral,
|
||||
@@ -27,7 +24,7 @@ import { increaseMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
export interface AssemblyArgs {}
|
||||
|
||||
export interface AssemblyResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
troop: Troop | null;
|
||||
troopMembers: Array<General<TriggerState>>;
|
||||
@@ -36,12 +33,8 @@ export interface AssemblyResolveContext<
|
||||
const ACTION_NAME = '집합';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
AssemblyArgs,
|
||||
AssemblyResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, AssemblyArgs, AssemblyResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_집합';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -50,23 +43,11 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: AssemblyArgs
|
||||
): Constraint[] {
|
||||
return [
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
mustBeTroopLeader(),
|
||||
reqTroopMembers(),
|
||||
];
|
||||
buildConstraints(_ctx: ConstraintContext, _args: AssemblyArgs): Constraint[] {
|
||||
return [notBeNeutral(), occupiedCity(), suppliedCity(), mustBeTroopLeader(), reqTroopMembers()];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AssemblyResolveContext<TriggerState>,
|
||||
_args: AssemblyArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: AssemblyResolveContext<TriggerState>, _args: AssemblyArgs): GeneralActionOutcome<TriggerState> {
|
||||
const city = context.city;
|
||||
if (!city) {
|
||||
context.addLog('도시 정보가 없어 집합을 진행할 수 없습니다.');
|
||||
@@ -81,26 +62,16 @@ export class ActionDefinition<
|
||||
context.addLog(`<G><b>${cityName}</b></>에서 집합을 실시했습니다.`);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
const targets = context.troopMembers.filter(
|
||||
(member) => member.cityId !== city.id
|
||||
);
|
||||
const targets = context.troopMembers.filter((member) => member.cityId !== city.id);
|
||||
for (const member of targets) {
|
||||
effects.push(createGeneralPatchEffect({ cityId: city.id } as Partial<General<TriggerState>>, member.id));
|
||||
effects.push(
|
||||
createGeneralPatchEffect(
|
||||
{ cityId: city.id } as Partial<General<TriggerState>>,
|
||||
member.id
|
||||
)
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`${troopName} 부대원들은 <G><b>${cityName}</b></>${josaRo} 집합되었습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
generalId: member.id,
|
||||
}
|
||||
)
|
||||
createLogEffect(`${troopName} 부대원들은 <G><b>${cityName}</b></>${josaRo} 집합되었습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
generalId: member.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,9 +87,9 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
const troopId = base.general.troopId;
|
||||
const troop = options.worldRef?.getTroopById(troopId) ?? null;
|
||||
const troopMembers =
|
||||
options.worldRef?.listGenerals().filter(
|
||||
(member) => member.troopId === troopId && member.id !== base.general.id
|
||||
) ?? [];
|
||||
options.worldRef
|
||||
?.listGenerals()
|
||||
.filter((member) => member.troopId === troopId && member.id !== base.general.id) ?? [];
|
||||
return {
|
||||
...base,
|
||||
troop,
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { City, General, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
@@ -20,10 +9,7 @@ import {
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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 {
|
||||
GeneralActionOutcome,
|
||||
@@ -56,7 +42,7 @@ export interface RecruitEnvironment {
|
||||
}
|
||||
|
||||
export interface RecruitResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
map: MapDefinition;
|
||||
unitSet: UnitSetDefinition;
|
||||
@@ -97,10 +83,7 @@ const readNationTech = (nation: Nation | null | undefined): number => {
|
||||
return typeof tech === 'number' ? tech : 0;
|
||||
};
|
||||
|
||||
const readCityTrust = (
|
||||
city: City,
|
||||
fallback: number
|
||||
): number => {
|
||||
const readCityTrust = (city: City, fallback: number): number => {
|
||||
const meta = city.meta as Record<string, unknown>;
|
||||
const trust = meta?.trust;
|
||||
return typeof trust === 'number' ? trust : fallback;
|
||||
@@ -136,10 +119,7 @@ const resolveCrewAmount = (args: Record<string, unknown>): number | null => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const buildCrewTypeContext = (
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): CrewTypeAvailabilityContext | null => {
|
||||
const buildCrewTypeContext = (ctx: ConstraintContext, view: StateView): CrewTypeAvailabilityContext | null => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
const general = view.get(generalReq) as General | null;
|
||||
if (!general) {
|
||||
@@ -147,8 +127,7 @@ const buildCrewTypeContext = (
|
||||
}
|
||||
const nationId = ctx.nationId ?? general.nationId;
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
|
||||
const nation =
|
||||
nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
|
||||
const nation = nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
|
||||
const map = ctx.env.map;
|
||||
const cities = ctx.env.cities;
|
||||
if (!map || !cities || !Array.isArray(cities)) {
|
||||
@@ -158,10 +137,9 @@ const buildCrewTypeContext = (
|
||||
typeof ctx.env.currentYear === 'number'
|
||||
? ctx.env.currentYear
|
||||
: typeof ctx.env.year === 'number'
|
||||
? ctx.env.year
|
||||
: undefined;
|
||||
const startYear =
|
||||
typeof ctx.env.startYear === 'number' ? ctx.env.startYear : undefined;
|
||||
? ctx.env.year
|
||||
: undefined;
|
||||
const startYear = typeof ctx.env.startYear === 'number' ? ctx.env.startYear : undefined;
|
||||
const result: CrewTypeAvailabilityContext = {
|
||||
general,
|
||||
nation,
|
||||
@@ -177,9 +155,7 @@ const buildCrewTypeContext = (
|
||||
return result;
|
||||
};
|
||||
|
||||
type RecruitCalcContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> = {
|
||||
type RecruitCalcContext<TriggerState extends GeneralTriggerState = GeneralTriggerState> = {
|
||||
general: General<TriggerState>;
|
||||
city?: City;
|
||||
nation?: Nation | null;
|
||||
@@ -196,12 +172,10 @@ const buildCalcContext = <TriggerState extends GeneralTriggerState>(
|
||||
}
|
||||
const nationId = ctx.nationId ?? general.nationId;
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
|
||||
const nation =
|
||||
nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
|
||||
const nation = nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
|
||||
const city =
|
||||
ctx.cityId !== undefined
|
||||
? ((view.get({ kind: 'city', id: ctx.cityId }) as City | null) ??
|
||||
undefined)
|
||||
? ((view.get({ kind: 'city', id: ctx.cityId }) as City | null) ?? undefined)
|
||||
: undefined;
|
||||
const result: RecruitCalcContext<TriggerState> = { general };
|
||||
if (city) {
|
||||
@@ -213,26 +187,19 @@ const buildCalcContext = <TriggerState extends GeneralTriggerState>(
|
||||
return result;
|
||||
};
|
||||
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
// 징병 명령의 비용/훈련/사기 계산을 담당한다.
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
private readonly env: RecruitEnvironment;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: RecruitEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
resolveLeadership(context: RecruitCalcContext<TriggerState>): number {
|
||||
const base = context.general.stats.leadership;
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStat(context, 'leadership', base)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStat(context, 'leadership', base));
|
||||
}
|
||||
|
||||
resolveCrewPlan(
|
||||
@@ -259,9 +226,7 @@ export class CommandResolver<
|
||||
const plan = this.resolveCrewPlan(context, crewTypeId, amount);
|
||||
const tech = readNationTech(context.nation ?? null);
|
||||
const costOffset = this.env.costOffset ?? DEFAULT_COST_OFFSET;
|
||||
const baseGold = crewType
|
||||
? crewType.cost * getTechCost(tech) * plan.applied / 100
|
||||
: 0;
|
||||
const baseGold = crewType ? (crewType.cost * getTechCost(tech) * plan.applied) / 100 : 0;
|
||||
const adjustedGold = this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
@@ -285,10 +250,7 @@ export class CommandResolver<
|
||||
};
|
||||
}
|
||||
|
||||
getTrain(
|
||||
context: RecruitCalcContext<TriggerState>,
|
||||
crewType?: { armType: number }
|
||||
): number {
|
||||
getTrain(context: RecruitCalcContext<TriggerState>, crewType?: { armType: number }): number {
|
||||
const base = this.env.defaultTrain ?? DEFAULT_TRAIN;
|
||||
return this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
@@ -299,10 +261,7 @@ export class CommandResolver<
|
||||
);
|
||||
}
|
||||
|
||||
getAtmos(
|
||||
context: RecruitCalcContext<TriggerState>,
|
||||
crewType?: { armType: number }
|
||||
): number {
|
||||
getAtmos(context: RecruitCalcContext<TriggerState>, crewType?: { armType: number }): number {
|
||||
const base = this.env.defaultAtmos ?? DEFAULT_ATMOS;
|
||||
return this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
@@ -313,40 +272,26 @@ export class CommandResolver<
|
||||
);
|
||||
}
|
||||
|
||||
getRecruitPopulation(
|
||||
context: RecruitCalcContext<TriggerState>,
|
||||
amount: number
|
||||
): number {
|
||||
const base = this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
'징집인구',
|
||||
'score',
|
||||
amount
|
||||
);
|
||||
getRecruitPopulation(context: RecruitCalcContext<TriggerState>, amount: number): number {
|
||||
const base = this.pipeline.onCalcDomestic(context, '징집인구', 'score', amount);
|
||||
return Math.round(base);
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, RecruitArgs> {
|
||||
readonly key = 'che_징병';
|
||||
// 징병 실행 결과를 계산하고 효과로 변환한다.
|
||||
private readonly env: RecruitEnvironment;
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: RecruitEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
|
||||
this.env = env;
|
||||
this.command = new CommandResolver(modules, env);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RecruitResolveContext<TriggerState>,
|
||||
args: RecruitArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: RecruitResolveContext<TriggerState>, args: RecruitArgs): GeneralActionOutcome<TriggerState> {
|
||||
const { general, city } = context;
|
||||
if (!city) {
|
||||
context.addLog('도시 정보가 없습니다.');
|
||||
@@ -376,27 +321,16 @@ export class ActionResolver<
|
||||
return { effects: [] };
|
||||
}
|
||||
|
||||
const plan = this.command.getCost(
|
||||
context,
|
||||
crewType.id,
|
||||
args.amount,
|
||||
crewType
|
||||
);
|
||||
const plan = this.command.getCost(context, crewType.id, args.amount, crewType);
|
||||
const setTrain = this.command.getTrain(context, crewType);
|
||||
const setAtmos = this.command.getAtmos(context, crewType);
|
||||
const appliedCrew = plan.applied;
|
||||
|
||||
const costOffset = this.env.costOffset ?? DEFAULT_COST_OFFSET;
|
||||
const recruitPop = this.command.getRecruitPopulation(
|
||||
context,
|
||||
appliedCrew
|
||||
);
|
||||
const recruitPop = this.command.getRecruitPopulation(context, appliedCrew);
|
||||
const nextPopulation = Math.max(city.population - recruitPop, 0);
|
||||
const baseTrust = readCityTrust(city, this.env.defaultTrust ?? DEFAULT_TRUST);
|
||||
const trustLoss =
|
||||
city.population > 0
|
||||
? (recruitPop / city.population) / costOffset * 100
|
||||
: 0;
|
||||
const trustLoss = city.population > 0 ? (recruitPop / city.population / costOffset) * 100 : 0;
|
||||
const nextTrust = Math.max(baseTrust - trustLoss, 0);
|
||||
|
||||
let nextCrewTypeId = general.crewTypeId;
|
||||
@@ -409,12 +343,10 @@ export class ActionResolver<
|
||||
if (crewType.id === general.crewTypeId && general.crew > 0) {
|
||||
nextCrew = general.crew + appliedCrew;
|
||||
nextTrain = Math.round(
|
||||
(general.crew * general.train + appliedCrew * setTrain) /
|
||||
(general.crew + appliedCrew)
|
||||
(general.crew * general.train + appliedCrew * setTrain) / (general.crew + appliedCrew)
|
||||
);
|
||||
nextAtmos = Math.round(
|
||||
(general.crew * general.atmos + appliedCrew * setAtmos) /
|
||||
(general.crew + appliedCrew)
|
||||
(general.crew * general.atmos + appliedCrew * setAtmos) / (general.crew + appliedCrew)
|
||||
);
|
||||
logMessage = `${crewLabel} 추가 ${ACTION_NAME}했습니다.`;
|
||||
} else {
|
||||
@@ -433,7 +365,7 @@ export class ActionResolver<
|
||||
// 직접 수정 (Immer Draft)
|
||||
city.population = nextPopulation;
|
||||
city.meta = {
|
||||
...city.meta as object,
|
||||
...(city.meta as object),
|
||||
trust: nextTrust,
|
||||
};
|
||||
|
||||
@@ -454,7 +386,7 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, RecruitArgs, RecruitResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_징병';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -462,10 +394,7 @@ export class ActionDefinition<
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
private readonly env: RecruitEnvironment;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: RecruitEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
this.resolver = new ActionResolver(modules, env);
|
||||
this.env = env;
|
||||
@@ -481,10 +410,7 @@ export class ActionDefinition<
|
||||
return { crewType: crewTypeId, amount };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
_args: RecruitArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(ctx: ConstraintContext, _args: RecruitArgs): Constraint[] {
|
||||
const requirements: RequirementKey[] = [
|
||||
{ kind: 'arg', key: 'crewType' },
|
||||
{ kind: 'arg', key: 'amount' },
|
||||
@@ -553,9 +479,7 @@ export class ActionDefinition<
|
||||
if (!availabilityContext) {
|
||||
return { kind: 'deny', reason: '병종 정보가 없습니다.' };
|
||||
}
|
||||
if (
|
||||
isCrewTypeAvailable(unitSet, crewTypeId, availabilityContext)
|
||||
) {
|
||||
if (isCrewTypeAvailable(unitSet, crewTypeId, availabilityContext)) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
return { kind: 'deny', reason: '현재 선택할 수 없는 병종입니다.' };
|
||||
@@ -565,18 +489,11 @@ export class ActionDefinition<
|
||||
const constraints: Constraint[] = [
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
reqCityCapacity(
|
||||
'population',
|
||||
'주민',
|
||||
minPopBase + resolveRequestedCrew(ctx)
|
||||
),
|
||||
reqCityCapacity('population', '주민', minPopBase + resolveRequestedCrew(ctx)),
|
||||
reqCityTrust(20),
|
||||
reqGeneralGold(getCost, requirements),
|
||||
reqGeneralRice(getRice, requirements),
|
||||
reqGeneralCrewMargin(
|
||||
(context) => resolveCrewTypeId(context.args),
|
||||
requirements
|
||||
),
|
||||
reqGeneralCrewMargin((context) => resolveCrewTypeId(context.args), requirements),
|
||||
];
|
||||
|
||||
if (ctx.mode === 'full') {
|
||||
@@ -586,10 +503,7 @@ export class ActionDefinition<
|
||||
return constraints;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RecruitResolveContext<TriggerState>,
|
||||
args: RecruitArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: RecruitResolveContext<TriggerState>, args: RecruitArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -614,6 +528,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: true,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], {}),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], {}),
|
||||
};
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
existsDestCity,
|
||||
hasRouteWithEnemy,
|
||||
@@ -37,22 +28,12 @@ import {
|
||||
import { JosaUtil, LiteHashDRBG } from '@sammo-ts/common';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import type {
|
||||
WarAftermathConfig,
|
||||
WarEngineConfig,
|
||||
WarTimeContext,
|
||||
} from '@sammo-ts/logic/war/types.js';
|
||||
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
||||
import { resolveWarAftermath } from '@sammo-ts/logic/war/aftermath.js';
|
||||
import { resolveWarBattle } from '@sammo-ts/logic/war/engine.js';
|
||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import {
|
||||
increaseMetaNumber,
|
||||
simpleSerialize,
|
||||
} from '@sammo-ts/logic/war/utils.js';
|
||||
import type {
|
||||
MapDefinition,
|
||||
UnitSetDefinition,
|
||||
} from '@sammo-ts/logic/world/types.js';
|
||||
import { increaseMetaNumber, simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
||||
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import {
|
||||
buildWarAftermathConfig,
|
||||
@@ -65,7 +46,7 @@ export interface DispatchArgs {
|
||||
}
|
||||
|
||||
export interface DispatchResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation?: Nation | null;
|
||||
@@ -197,9 +178,7 @@ const pickCandidateCity = (
|
||||
return { cityId, isEnemy: true, minDist };
|
||||
}
|
||||
const fallback = distanceList.get(minDist) ?? [];
|
||||
const friendly = fallback.filter(
|
||||
([, nationId]) => nationId === attackerNationId
|
||||
);
|
||||
const friendly = fallback.filter(([, nationId]) => nationId === attackerNationId);
|
||||
if (friendly.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -216,10 +195,7 @@ const getRequiredRice = (ctx: ConstraintContext, view: StateView): number => {
|
||||
return Math.round(general.crew / 100);
|
||||
};
|
||||
|
||||
const resolveCrewTypeArm = (
|
||||
unitSet: UnitSetDefinition,
|
||||
crewTypeId: number
|
||||
): number | null => {
|
||||
const resolveCrewTypeArm = (unitSet: UnitSetDefinition, crewTypeId: number): number | null => {
|
||||
const crewTypes = unitSet.crewTypes ?? [];
|
||||
const crewType = crewTypes.find((entry) => entry.id === crewTypeId);
|
||||
if (!crewType) {
|
||||
@@ -260,13 +236,8 @@ const cloneNation = (nation: Nation): Nation => ({
|
||||
});
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements
|
||||
GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DispatchArgs,
|
||||
DispatchResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DispatchArgs, DispatchResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_출병';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly warModules: Array<WarActionModule<TriggerState>>;
|
||||
@@ -286,10 +257,7 @@ export class ActionDefinition<
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DispatchArgs): Constraint[] {
|
||||
const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0;
|
||||
const openingPartYear =
|
||||
typeof _ctx.env.openingPartYear === 'number'
|
||||
? _ctx.env.openingPartYear
|
||||
: 0;
|
||||
const openingPartYear = typeof _ctx.env.openingPartYear === 'number' ? _ctx.env.openingPartYear : 0;
|
||||
return [
|
||||
notOpeningPart(relYear, openingPartYear),
|
||||
notSameDestCity(),
|
||||
@@ -304,10 +272,7 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DispatchResolveContext<TriggerState>,
|
||||
args: DispatchArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: DispatchResolveContext<TriggerState>, args: DispatchArgs): GeneralActionOutcome<TriggerState> {
|
||||
void args;
|
||||
const attackerCity = context.city;
|
||||
if (!attackerCity) {
|
||||
@@ -322,10 +287,7 @@ export class ActionDefinition<
|
||||
const unitSet = context.unitSet;
|
||||
const time = context.time;
|
||||
const diplomacy = context.diplomacy ?? [];
|
||||
const allowedNationIds = buildAllowedNationIds(
|
||||
attackerNation.id,
|
||||
diplomacy
|
||||
);
|
||||
const allowedNationIds = buildAllowedNationIds(attackerNation.id, diplomacy);
|
||||
const mapIndex = context.map ? buildMapIndex(context.map) : null;
|
||||
|
||||
let defenderCityId = finalTargetCity.id;
|
||||
@@ -345,11 +307,7 @@ export class ActionDefinition<
|
||||
mapIndex,
|
||||
allowedCityIds
|
||||
);
|
||||
const picked = pickCandidateCity(
|
||||
context.rng,
|
||||
distanceList,
|
||||
attackerNation.id
|
||||
);
|
||||
const picked = pickCandidateCity(context.rng, distanceList, attackerNation.id);
|
||||
if (!picked) {
|
||||
context.addLog('경로에 도달할 방법이 없습니다.');
|
||||
return { effects: [] };
|
||||
@@ -362,15 +320,12 @@ export class ActionDefinition<
|
||||
const destCity =
|
||||
defenderCityId === finalTargetCity.id
|
||||
? finalTargetCity
|
||||
: context.cities.find((city) => city.id === defenderCityId) ??
|
||||
finalTargetCity;
|
||||
: (context.cities.find((city) => city.id === defenderCityId) ?? finalTargetCity);
|
||||
|
||||
if (!isEnemyTarget && destCity.nationId === attackerNation.id) {
|
||||
const josaRo = JosaUtil.pick(destCity.name, '로');
|
||||
if (finalTargetCity.id === destCity.id) {
|
||||
context.addLog(
|
||||
`본국입니다. <G><b>${destCity.name}</b></>${josaRo} 이동합니다.`
|
||||
);
|
||||
context.addLog(`본국입니다. <G><b>${destCity.name}</b></>${josaRo} 이동합니다.`);
|
||||
} else {
|
||||
const targetName = finalTargetCity.name;
|
||||
const josaRoTarget = JosaUtil.pick(targetName, '로');
|
||||
@@ -410,11 +365,7 @@ export class ActionDefinition<
|
||||
|
||||
const armType = resolveCrewTypeArm(unitSet, context.general.crewTypeId);
|
||||
if (armType !== null) {
|
||||
increaseMetaNumber(
|
||||
context.general.meta,
|
||||
`dex${armType}`,
|
||||
context.general.crew / 100
|
||||
);
|
||||
increaseMetaNumber(context.general.meta, `dex${armType}`, context.general.crew / 100);
|
||||
}
|
||||
|
||||
const cities = context.cities.map(cloneCity);
|
||||
@@ -425,15 +376,10 @@ export class ActionDefinition<
|
||||
const nationMap = new Map(nations.map((nation) => [nation.id, nation]));
|
||||
|
||||
const defenderCity = cityMap.get(destCity.id) ?? cloneCity(destCity);
|
||||
const defenderNation =
|
||||
defenderCity.nationId > 0
|
||||
? nationMap.get(defenderCity.nationId) ?? null
|
||||
: null;
|
||||
const defenderNation = defenderCity.nationId > 0 ? (nationMap.get(defenderCity.nationId) ?? null) : null;
|
||||
|
||||
const defenderGenerals = generals.filter(
|
||||
(general) =>
|
||||
general.cityId === defenderCity.id &&
|
||||
general.nationId === defenderCity.nationId
|
||||
(general) => general.cityId === defenderCity.id && general.nationId === defenderCity.nationId
|
||||
);
|
||||
|
||||
const battle = resolveWarBattle({
|
||||
@@ -556,16 +502,10 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
if (!destCity) {
|
||||
return null;
|
||||
}
|
||||
const destNation =
|
||||
destCity.nationId > 0
|
||||
? options.worldRef.getNationById(destCity.nationId)
|
||||
: null;
|
||||
const destNation = destCity.nationId > 0 ? options.worldRef.getNationById(destCity.nationId) : null;
|
||||
const diplomacy = options.worldRef.listDiplomacy();
|
||||
const warConfig = buildWarConfig(options.scenarioConfig, options.unitSet);
|
||||
const aftermathConfig = buildWarAftermathConfig(
|
||||
options.scenarioConfig,
|
||||
warConfig.castleCrewTypeId
|
||||
);
|
||||
const aftermathConfig = buildWarAftermathConfig(options.scenarioConfig, warConfig.castleCrewTypeId);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
@@ -588,6 +528,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '군사',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.warActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.warActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||
super(
|
||||
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '내정',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition({ develCost: env.develCost }),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, General, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
disallowDiplomacyBetweenStatus,
|
||||
@@ -19,10 +13,7 @@ import {
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -30,10 +21,7 @@ import type {
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import {
|
||||
createCityPatchEffect,
|
||||
createGeneralPatchEffect,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createCityPatchEffect, createGeneralPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
@@ -54,18 +42,12 @@ export interface FireAttackEnvironment {
|
||||
maxSuccessProbability?: number;
|
||||
statKey?: 'leadership' | 'strength' | 'intelligence';
|
||||
getDistance?: (sourceCityId: number, destCityId: number) => number | null;
|
||||
getDefenceCorrection?: (
|
||||
context: FireAttackContext,
|
||||
defender: General
|
||||
) => number;
|
||||
getInjuryProbability?: (
|
||||
context: FireAttackContext,
|
||||
defender: General
|
||||
) => number;
|
||||
getDefenceCorrection?: (context: FireAttackContext, defender: General) => number;
|
||||
getInjuryProbability?: (context: FireAttackContext, defender: General) => number;
|
||||
}
|
||||
|
||||
export interface FireAttackContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionContext<TriggerState> {
|
||||
general: General<TriggerState>;
|
||||
city: City;
|
||||
@@ -76,16 +58,14 @@ export interface FireAttackContext<
|
||||
}
|
||||
|
||||
export interface FireAttackResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation?: Nation | null;
|
||||
destGenerals: General<TriggerState>[];
|
||||
}
|
||||
|
||||
export interface FireAttackResult<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface FireAttackResult<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
success: boolean;
|
||||
probability: number;
|
||||
distance: number;
|
||||
@@ -109,16 +89,9 @@ const DEFAULT_MAX_PROB = 0.5;
|
||||
const INJURY_MAX = 80;
|
||||
const CITY_STATE_BURNING = 32;
|
||||
|
||||
const randomRangeInt = (
|
||||
rng: RandomGenerator,
|
||||
min: number,
|
||||
max: number
|
||||
): number => rng.nextInt(min, max + 1);
|
||||
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
|
||||
|
||||
const getStatValue = (
|
||||
general: General,
|
||||
statKey: 'leadership' | 'strength' | 'intelligence'
|
||||
): number => {
|
||||
const getStatValue = (general: General, statKey: 'leadership' | 'strength' | 'intelligence'): number => {
|
||||
if (statKey === 'leadership') {
|
||||
return general.stats.leadership;
|
||||
}
|
||||
@@ -138,17 +111,12 @@ const addMetaNumber = (
|
||||
};
|
||||
|
||||
// 화계 성공/실패 및 피해량 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
private readonly env: FireAttackEnvironment;
|
||||
private readonly statKey: 'leadership' | 'strength' | 'intelligence';
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: FireAttackEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
|
||||
this.pipeline = new GeneralActionPipeline(modules);
|
||||
this.env = env;
|
||||
this.statKey = env.statKey ?? 'intelligence';
|
||||
@@ -159,22 +127,13 @@ export class CommandResolver<
|
||||
return { gold: cost, rice: cost };
|
||||
}
|
||||
|
||||
private calcAttackProb(
|
||||
context: FireAttackContext<TriggerState>
|
||||
): number {
|
||||
private calcAttackProb(context: FireAttackContext<TriggerState>): number {
|
||||
const stat = getStatValue(context.general, this.statKey);
|
||||
let prob = stat / this.env.sabotageProbCoefByStat;
|
||||
return this.pipeline.onCalcDomestic(
|
||||
context,
|
||||
ACTION_KEY,
|
||||
'success',
|
||||
prob
|
||||
);
|
||||
const prob = stat / this.env.sabotageProbCoefByStat;
|
||||
return this.pipeline.onCalcDomestic(context, ACTION_KEY, 'success', prob);
|
||||
}
|
||||
|
||||
private calcDefenceProb(
|
||||
context: FireAttackContext<TriggerState>
|
||||
): number {
|
||||
private calcDefenceProb(context: FireAttackContext<TriggerState>): number {
|
||||
const destNationId = context.destCity.nationId;
|
||||
let maxStat = 0;
|
||||
let probCorrection = 0;
|
||||
@@ -185,19 +144,13 @@ export class CommandResolver<
|
||||
continue;
|
||||
}
|
||||
affectCount += 1;
|
||||
maxStat = Math.max(
|
||||
maxStat,
|
||||
getStatValue(defender, this.statKey)
|
||||
);
|
||||
probCorrection +=
|
||||
this.env.getDefenceCorrection?.(context, defender) ?? 0;
|
||||
maxStat = Math.max(maxStat, getStatValue(defender, this.statKey));
|
||||
probCorrection += this.env.getDefenceCorrection?.(context, defender) ?? 0;
|
||||
}
|
||||
|
||||
let prob = maxStat / this.env.sabotageProbCoefByStat;
|
||||
prob += probCorrection;
|
||||
prob +=
|
||||
(Math.log2(affectCount + 1) - 1.25) *
|
||||
this.env.sabotageDefenceCoefByGeneralCount;
|
||||
prob += (Math.log2(affectCount + 1) - 1.25) * this.env.sabotageDefenceCoefByGeneralCount;
|
||||
|
||||
prob += context.destCity.security / context.destCity.securityMax / 5;
|
||||
prob += context.destCity.supplyState ? 0.1 : 0;
|
||||
@@ -205,25 +158,15 @@ export class CommandResolver<
|
||||
return prob;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: FireAttackContext<TriggerState>,
|
||||
rng: RandomGenerator
|
||||
): FireAttackResult<TriggerState> {
|
||||
resolve(context: FireAttackContext<TriggerState>, rng: RandomGenerator): FireAttackResult<TriggerState> {
|
||||
const { gold: costGold, rice: costRice } = this.getCost();
|
||||
const distance =
|
||||
this.env.getDistance?.(context.general.cityId, context.destCity.id) ??
|
||||
99;
|
||||
const distance = this.env.getDistance?.(context.general.cityId, context.destCity.id) ?? 99;
|
||||
|
||||
const attackProb = this.calcAttackProb(context);
|
||||
const defenceProb = this.calcDefenceProb(context);
|
||||
let probability =
|
||||
this.env.sabotageDefaultProb + attackProb - defenceProb;
|
||||
let probability = this.env.sabotageDefaultProb + attackProb - defenceProb;
|
||||
probability /= distance;
|
||||
probability = clamp(
|
||||
probability,
|
||||
0,
|
||||
this.env.maxSuccessProbability ?? DEFAULT_MAX_PROB
|
||||
);
|
||||
probability = clamp(probability, 0, this.env.maxSuccessProbability ?? DEFAULT_MAX_PROB);
|
||||
|
||||
const success = rng.nextBool(probability);
|
||||
const expRange: [number, number] = success ? [201, 300] : [1, 100];
|
||||
@@ -248,20 +191,12 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
const agriDamage = clamp(
|
||||
randomRangeInt(
|
||||
rng,
|
||||
this.env.sabotageDamageMin,
|
||||
this.env.sabotageDamageMax
|
||||
),
|
||||
randomRangeInt(rng, this.env.sabotageDamageMin, this.env.sabotageDamageMax),
|
||||
0,
|
||||
context.destCity.agriculture
|
||||
);
|
||||
const commDamage = clamp(
|
||||
randomRangeInt(
|
||||
rng,
|
||||
this.env.sabotageDamageMin,
|
||||
this.env.sabotageDamageMax
|
||||
),
|
||||
randomRangeInt(rng, this.env.sabotageDamageMin, this.env.sabotageDamageMax),
|
||||
0,
|
||||
context.destCity.commerce
|
||||
);
|
||||
@@ -275,9 +210,7 @@ export class CommandResolver<
|
||||
if (defender.nationId !== context.destCity.nationId) {
|
||||
continue;
|
||||
}
|
||||
const injuryProb =
|
||||
this.env.getInjuryProbability?.(context, defender) ??
|
||||
injuryProbDefault;
|
||||
const injuryProb = this.env.getInjuryProbability?.(context, defender) ?? injuryProbDefault;
|
||||
if (!rng.nextBool(injuryProb)) {
|
||||
continue;
|
||||
}
|
||||
@@ -285,11 +218,7 @@ export class CommandResolver<
|
||||
injuredGenerals.push({
|
||||
id: defender.id,
|
||||
patch: {
|
||||
injury: clamp(
|
||||
defender.injury + injuryAmount,
|
||||
0,
|
||||
INJURY_MAX
|
||||
),
|
||||
injury: clamp(defender.injury + injuryAmount, 0, INJURY_MAX),
|
||||
crew: Math.floor(defender.crew * 0.98),
|
||||
train: Math.floor(defender.train * 0.98),
|
||||
},
|
||||
@@ -313,15 +242,12 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, FireAttackArgs> {
|
||||
readonly key = 'che_화계';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: FireAttackEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
}
|
||||
|
||||
@@ -355,14 +281,8 @@ export class ActionResolver<
|
||||
const nextExperience = general.experience + result.exp;
|
||||
const nextDedication = general.dedication + result.dedication;
|
||||
|
||||
const metaWithStatExp = addMetaNumber(
|
||||
general.meta,
|
||||
STAT_EXP_KEY,
|
||||
1
|
||||
);
|
||||
const metaUpdated = result.success
|
||||
? addMetaNumber(metaWithStatExp, 'firenum', 1)
|
||||
: metaWithStatExp;
|
||||
const metaWithStatExp = addMetaNumber(general.meta, STAT_EXP_KEY, 1);
|
||||
const metaUpdated = result.success ? addMetaNumber(metaWithStatExp, 'firenum', 1) : metaWithStatExp;
|
||||
|
||||
// 직접 수정 (Immer Draft)
|
||||
general.gold = nextGold;
|
||||
@@ -372,12 +292,9 @@ export class ActionResolver<
|
||||
general.meta = metaUpdated;
|
||||
|
||||
if (!result.success) {
|
||||
context.addLog(
|
||||
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 실패했습니다.`,
|
||||
{
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
);
|
||||
context.addLog(`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 실패했습니다.`, {
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
return { effects: [] };
|
||||
}
|
||||
|
||||
@@ -398,20 +315,14 @@ export class ActionResolver<
|
||||
)
|
||||
);
|
||||
|
||||
context.addLog(
|
||||
`<G><b>${context.destCity.name}</b></>이 불타고 있습니다.`,
|
||||
{
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.SUMMARY,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
);
|
||||
context.addLog(
|
||||
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 성공했습니다.`,
|
||||
{
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
);
|
||||
context.addLog(`<G><b>${context.destCity.name}</b></>이 불타고 있습니다.`, {
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.SUMMARY,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
context.addLog(`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 성공했습니다.`, {
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
context.addLog(
|
||||
`도시의 농업이 <C>${result.agriDamage}</>, 상업이 <C>${result.commDamage}</>만큼 감소하고, 장수 <C>${result.injuryCount}</>명이 부상 당했습니다.`,
|
||||
{
|
||||
@@ -421,16 +332,11 @@ export class ActionResolver<
|
||||
|
||||
for (const injured of result.injuredGenerals) {
|
||||
// 타겟 장수는 Draft가 아니므로 Effect 반환
|
||||
effects.push(
|
||||
createGeneralPatchEffect(injured.patch, injured.id)
|
||||
);
|
||||
context.addLog(
|
||||
`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`,
|
||||
{
|
||||
generalId: injured.id,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
);
|
||||
effects.push(createGeneralPatchEffect(injured.patch, injured.id));
|
||||
context.addLog(`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`, {
|
||||
generalId: injured.id,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
}
|
||||
|
||||
return { effects };
|
||||
@@ -438,17 +344,14 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, FireAttackArgs, FireAttackResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_화계';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: FireAttackEnvironment
|
||||
) {
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
this.resolver = new ActionResolver(modules, env);
|
||||
}
|
||||
@@ -464,10 +367,7 @@ export class ActionDefinition<
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: FireAttackArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: FireAttackArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
const { gold, rice } = this.command.getCost();
|
||||
@@ -486,10 +386,7 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: FireAttackResolveContext<TriggerState>,
|
||||
args: FireAttackArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: FireAttackResolveContext<TriggerState>, args: FireAttackArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -502,6 +399,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '계략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
};
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
@@ -30,7 +21,7 @@ const DEFAULT_TRAIN_DELTA = 5;
|
||||
const DEFAULT_MAX_TRAIN = 100;
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, TrainingArgs> {
|
||||
public readonly key = 'che_훈련';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -45,12 +36,8 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: TrainingArgs
|
||||
): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.costGold ?? 0;
|
||||
buildConstraints(_ctx: ConstraintContext, _args: TrainingArgs): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
|
||||
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
|
||||
}
|
||||
|
||||
@@ -63,10 +50,7 @@ export class ActionDefinition<
|
||||
this.env.maxTrainByCommand && this.env.maxTrainByCommand > 0
|
||||
? this.env.maxTrainByCommand
|
||||
: DEFAULT_MAX_TRAIN;
|
||||
const delta =
|
||||
this.env.trainDelta && this.env.trainDelta > 0
|
||||
? this.env.trainDelta
|
||||
: DEFAULT_TRAIN_DELTA;
|
||||
const delta = this.env.trainDelta && this.env.trainDelta > 0 ? this.env.trainDelta : DEFAULT_TRAIN_DELTA;
|
||||
const nextTrain = clamp(general.train + delta, 0, maxTrain);
|
||||
const applied = nextTrain - general.train;
|
||||
const costGold = this.env.costGold ?? 0;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type {
|
||||
City,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { City, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
notWanderingNation,
|
||||
@@ -16,10 +9,7 @@ import {
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { clamp } from 'es-toolkit';
|
||||
|
||||
export interface CityDevelopmentArgs {}
|
||||
@@ -44,7 +34,7 @@ const readNumber = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
|
||||
export class CityDevelopmentActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, CityDevelopmentArgs> {
|
||||
public readonly key: string;
|
||||
public readonly name: string;
|
||||
@@ -63,23 +53,15 @@ export class CityDevelopmentActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: CityDevelopmentArgs
|
||||
): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||
this.env.develCost ?? 0;
|
||||
buildConstraints(_ctx: ConstraintContext, _args: CityDevelopmentArgs): Constraint[] {
|
||||
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
|
||||
|
||||
return [
|
||||
notBeNeutral(),
|
||||
notWanderingNation(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
remainCityCapacityByMax(
|
||||
this.config.statKey,
|
||||
this.config.maxKey,
|
||||
this.config.label
|
||||
),
|
||||
remainCityCapacityByMax(this.config.statKey, this.config.maxKey, this.config.label),
|
||||
reqGeneralGold(getRequiredGold),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -27,21 +27,15 @@ export const GENERAL_TURN_COMMAND_KEYS = [
|
||||
'휴식',
|
||||
] as const;
|
||||
|
||||
export type GeneralTurnCommandKey =
|
||||
(typeof GENERAL_TURN_COMMAND_KEYS)[number];
|
||||
export type GeneralTurnCommandKey = (typeof GENERAL_TURN_COMMAND_KEYS)[number];
|
||||
|
||||
export type GeneralTurnCommandSpec =
|
||||
TurnCommandSpecBase<GeneralTurnCommandKey>;
|
||||
export type GeneralTurnCommandSpec = TurnCommandSpecBase<GeneralTurnCommandKey>;
|
||||
|
||||
export type GeneralTurnCommandModule =
|
||||
TurnCommandModule<GeneralTurnCommandSpec>;
|
||||
export type GeneralTurnCommandModule = TurnCommandModule<GeneralTurnCommandSpec>;
|
||||
|
||||
export type GeneralTurnCommandImporter = () => Promise<GeneralTurnCommandModule>;
|
||||
|
||||
const defaultImporters: Record<
|
||||
GeneralTurnCommandKey,
|
||||
GeneralTurnCommandImporter
|
||||
> = {
|
||||
const defaultImporters: Record<GeneralTurnCommandKey, GeneralTurnCommandImporter> = {
|
||||
che_거병: async () => import('./che_거병.js'),
|
||||
che_임관: async () => import('./che_임관.js'),
|
||||
che_건국: async () => import('./che_건국.js'),
|
||||
@@ -68,28 +62,17 @@ const defaultImporters: Record<
|
||||
휴식: async () => import('./휴식.js'),
|
||||
};
|
||||
|
||||
export const isGeneralTurnCommandKey = (
|
||||
value: string
|
||||
): value is GeneralTurnCommandKey =>
|
||||
export const isGeneralTurnCommandKey = (value: string): value is GeneralTurnCommandKey =>
|
||||
GENERAL_TURN_COMMAND_KEYS.includes(value as GeneralTurnCommandKey);
|
||||
|
||||
|
||||
export class GeneralTurnCommandLoader {
|
||||
private readonly cache = new Map<
|
||||
GeneralTurnCommandKey,
|
||||
Promise<GeneralTurnCommandModule>
|
||||
>();
|
||||
private readonly cache = new Map<GeneralTurnCommandKey, Promise<GeneralTurnCommandModule>>();
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<
|
||||
GeneralTurnCommandKey,
|
||||
GeneralTurnCommandImporter
|
||||
> = defaultImporters
|
||||
) { }
|
||||
private readonly importers: Record<GeneralTurnCommandKey, GeneralTurnCommandImporter> = defaultImporters
|
||||
) {}
|
||||
|
||||
async load(
|
||||
key: GeneralTurnCommandKey
|
||||
): Promise<GeneralTurnCommandModule> {
|
||||
async load(key: GeneralTurnCommandKey): Promise<GeneralTurnCommandModule> {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
|
||||
@@ -6,9 +6,7 @@ import type {
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
|
||||
export interface GeneralRecruitmentInput<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface GeneralRecruitmentInput<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
id: number;
|
||||
name: string;
|
||||
nationId: number;
|
||||
@@ -52,9 +50,7 @@ const createEmptyRole = (): GeneralRole => ({
|
||||
});
|
||||
|
||||
// 모집/탐색 등으로 생성되는 장수의 기본 모델을 구성한다.
|
||||
export const buildRecruitmentGeneral = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const buildRecruitmentGeneral = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
input: GeneralRecruitmentInput<TriggerState>
|
||||
): General<TriggerState> => ({
|
||||
id: input.id,
|
||||
@@ -79,8 +75,6 @@ export const buildRecruitmentGeneral = <
|
||||
atmos: input.atmos ?? 0,
|
||||
age: input.age,
|
||||
npcState: input.npcState,
|
||||
triggerState:
|
||||
input.triggerState ??
|
||||
(createEmptyTriggerState() as TriggerState),
|
||||
triggerState: input.triggerState ?? (createEmptyTriggerState() as TriggerState),
|
||||
meta: input.meta ?? {},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
@@ -21,14 +16,11 @@ export interface RestArgs {}
|
||||
const ACTION_NAME = '휴식';
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, RestArgs> {
|
||||
readonly key = '휴식';
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
_args: RestArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: GeneralActionResolveContext<TriggerState>, _args: RestArgs): GeneralActionOutcome<TriggerState> {
|
||||
context.addLog('아무것도 실행하지 않았습니다.', {
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
@@ -38,7 +30,7 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, RestArgs> {
|
||||
public readonly key = '휴식';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -49,19 +41,13 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: RestArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: RestArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
args: RestArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: GeneralActionResolveContext<TriggerState>, args: RestArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
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,
|
||||
@@ -11,10 +7,7 @@ import {
|
||||
existsDestNation,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -22,10 +15,7 @@ import type {
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} 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 type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
@@ -38,7 +28,7 @@ export interface RaidArgs {
|
||||
}
|
||||
|
||||
export interface RaidResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
diplomacy: { state: number; term: number };
|
||||
@@ -62,9 +52,7 @@ const parseNationId = (raw: unknown): number | null => {
|
||||
};
|
||||
|
||||
// 급습 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
@@ -72,20 +60,13 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
getGlobalDelay(context: RaidResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
}
|
||||
|
||||
// 급습 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, RaidArgs> {
|
||||
readonly key = 'che_급습';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
@@ -94,10 +75,7 @@ export class ActionResolver<
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RaidResolveContext<TriggerState>,
|
||||
_args: RaidArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: RaidResolveContext<TriggerState>, _args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
@@ -111,29 +89,18 @@ export class ActionResolver<
|
||||
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,
|
||||
}
|
||||
);
|
||||
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,
|
||||
}
|
||||
),
|
||||
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) {
|
||||
@@ -195,12 +162,8 @@ export class ActionResolver<
|
||||
|
||||
// 급습 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
RaidArgs,
|
||||
RaidResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, RaidArgs, RaidResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_급습';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -225,19 +188,12 @@ export class ActionDefinition<
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
existsDestNation(),
|
||||
allowDiplomacyWithTerm(
|
||||
1,
|
||||
12,
|
||||
'선포 12개월 이상인 상대국에만 가능합니다.'
|
||||
),
|
||||
allowDiplomacyWithTerm(1, 12, '선포 12개월 이상인 상대국에만 가능합니다.'),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: RaidResolveContext<TriggerState>,
|
||||
args: RaidArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: RaidResolveContext<TriggerState>, args: RaidArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -263,12 +219,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
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
|
||||
);
|
||||
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
|
||||
const destNationGenerals = generals.filter((general) => general.nationId === destNationId);
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
@@ -284,6 +236,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { City, General, GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
beChief,
|
||||
@@ -40,7 +32,7 @@ export interface AssignmentArgs {
|
||||
}
|
||||
|
||||
export interface AssignmentResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destGeneral: General<TriggerState>;
|
||||
destCity: City;
|
||||
@@ -57,17 +49,14 @@ export interface AssignmentEnvironment {
|
||||
|
||||
const ACTION_NAME = '발령';
|
||||
|
||||
const joinYearMonth = (year: number, month: number): number =>
|
||||
year * 12 + month - 1;
|
||||
const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const cutTurn = (time: Date, turnTermMinutes: number): number => {
|
||||
const turnMs = turnTermMinutes * 60 * 1000;
|
||||
return Math.floor(time.getTime() / turnMs);
|
||||
};
|
||||
|
||||
const resolveLastAssignment = (
|
||||
context: AssignmentResolveContext
|
||||
): number => {
|
||||
const resolveLastAssignment = (context: AssignmentResolveContext): number => {
|
||||
let yearMonth = joinYearMonth(context.currentYear, context.currentMonth);
|
||||
const term = context.turnTermMinutes;
|
||||
const srcTime = context.generalTurnTime;
|
||||
@@ -91,7 +80,7 @@ const addMetaValue = (
|
||||
|
||||
// 발령 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, AssignmentArgs> {
|
||||
readonly key = 'che_발령';
|
||||
private readonly env: AssignmentEnvironment;
|
||||
@@ -107,9 +96,7 @@ export class ActionResolver<
|
||||
void _args;
|
||||
const destGeneral = context.destGeneral;
|
||||
const destCity = context.destCity;
|
||||
const cityName = this.env.formatCityName
|
||||
? this.env.formatCityName(destCity)
|
||||
: destCity.name;
|
||||
const cityName = this.env.formatCityName ? this.env.formatCityName(destCity) : destCity.name;
|
||||
const cityJosa = JosaUtil.pick(cityName, '로');
|
||||
const generalJosa = JosaUtil.pick(destGeneral.name, '을');
|
||||
const yearMonth = resolveLastAssignment(context);
|
||||
@@ -125,15 +112,12 @@ export class ActionResolver<
|
||||
];
|
||||
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></>${cityJosa} 발령됐습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: destGeneral.id,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
createLogEffect(`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></>${cityJosa} 발령됐습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: destGeneral.id,
|
||||
format: LogFormat.MONTH,
|
||||
})
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
@@ -151,12 +135,8 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
AssignmentArgs,
|
||||
AssignmentResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, AssignmentArgs, AssignmentResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_발령';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -185,10 +165,7 @@ export class ActionDefinition<
|
||||
};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
_args: AssignmentArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(ctx: ConstraintContext, _args: AssignmentArgs): Constraint[] {
|
||||
void _args;
|
||||
if (ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
@@ -205,10 +182,7 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AssignmentResolveContext<TriggerState>,
|
||||
args: AssignmentArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: AssignmentResolveContext<TriggerState>, args: AssignmentArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
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,
|
||||
@@ -10,10 +6,7 @@ import {
|
||||
occupiedCity,
|
||||
occupiedDestCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -33,7 +26,7 @@ export interface MobilizePeopleArgs {
|
||||
}
|
||||
|
||||
export interface MobilizePeopleResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
@@ -54,9 +47,7 @@ const parseCityId = (raw: unknown): number | null => {
|
||||
};
|
||||
|
||||
// 백성동원 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
@@ -64,20 +55,13 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
getGlobalDelay(context: MobilizePeopleResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
}
|
||||
|
||||
// 백성동원 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, MobilizePeopleArgs> {
|
||||
readonly key = 'che_백성동원';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
@@ -101,13 +85,10 @@ export class ActionResolver<
|
||||
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,
|
||||
}
|
||||
);
|
||||
context.addLog(`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
});
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
@@ -125,14 +106,8 @@ export class ActionResolver<
|
||||
);
|
||||
}
|
||||
|
||||
const nextDefence = Math.max(
|
||||
context.destCity.defence,
|
||||
context.destCity.defenceMax * DEFENCE_RATE
|
||||
);
|
||||
const nextWall = Math.max(
|
||||
context.destCity.wall,
|
||||
context.destCity.wallMax * DEFENCE_RATE
|
||||
);
|
||||
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(
|
||||
{
|
||||
@@ -165,12 +140,8 @@ export class ActionResolver<
|
||||
|
||||
// 백성동원 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
MobilizePeopleArgs,
|
||||
MobilizePeopleResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, MobilizePeopleArgs, MobilizePeopleResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_백성동원';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -188,18 +159,10 @@ export class ActionDefinition<
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: MobilizePeopleArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: MobilizePeopleArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
occupiedDestCity(),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
return [occupiedCity(), beChief(), occupiedDestCity(), availableStrategicCommand()];
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -224,9 +187,7 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
if (!destCity) {
|
||||
return null;
|
||||
}
|
||||
const friendlyGenerals = worldRef
|
||||
.listGenerals()
|
||||
.filter((general) => general.nationId === base.general.nationId);
|
||||
const friendlyGenerals = worldRef.listGenerals().filter((general) => general.nationId === base.general.nationId);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
@@ -239,6 +200,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
beChief,
|
||||
@@ -19,10 +13,7 @@ import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import {
|
||||
createGeneralPatchEffect,
|
||||
createLogEffect,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createGeneralPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.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 { NationTurnCommandSpec } from './index.js';
|
||||
@@ -34,7 +25,7 @@ export interface TroopKickArgs {
|
||||
}
|
||||
|
||||
export interface TroopKickResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destGeneral: General<TriggerState>;
|
||||
}
|
||||
@@ -42,12 +33,8 @@ export interface TroopKickResolveContext<
|
||||
const ACTION_NAME = '부대 탈퇴 지시';
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
TroopKickArgs,
|
||||
TroopKickResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, TroopKickArgs, TroopKickResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_부대탈퇴지시';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -65,25 +52,14 @@ export class ActionDefinition<
|
||||
return { destGeneralId: data.destGeneralId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
_args: TroopKickArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(ctx: ConstraintContext, _args: TroopKickArgs): Constraint[] {
|
||||
if (ctx.destGeneralId !== undefined && ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
}
|
||||
return [
|
||||
notBeNeutral(),
|
||||
beChief(),
|
||||
existsDestGeneral(),
|
||||
friendlyDestGeneral(),
|
||||
];
|
||||
return [notBeNeutral(), beChief(), existsDestGeneral(), friendlyDestGeneral()];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: TroopKickResolveContext<TriggerState>,
|
||||
_args: TroopKickArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: TroopKickResolveContext<TriggerState>, _args: TroopKickArgs): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
const destGeneral = context.destGeneral;
|
||||
const destGeneralName = destGeneral.name;
|
||||
@@ -91,46 +67,31 @@ export class ActionDefinition<
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
if (destGeneral.troopId === 0) {
|
||||
context.addLog(
|
||||
`<Y>${destGeneralName}</>${josaUn} 부대원이 아닙니다.`
|
||||
);
|
||||
context.addLog(`<Y>${destGeneralName}</>${josaUn} 부대원이 아닙니다.`);
|
||||
return { effects };
|
||||
}
|
||||
|
||||
if (destGeneral.troopId === destGeneral.id) {
|
||||
context.addLog(
|
||||
`<Y>${destGeneralName}</>${josaUn} 부대장입니다.`
|
||||
);
|
||||
context.addLog(`<Y>${destGeneralName}</>${josaUn} 부대장입니다.`);
|
||||
return { effects };
|
||||
}
|
||||
|
||||
effects.push(
|
||||
createGeneralPatchEffect(
|
||||
{ troopId: 0 } as Partial<General<TriggerState>>,
|
||||
destGeneral.id
|
||||
)
|
||||
);
|
||||
effects.push(createGeneralPatchEffect({ troopId: 0 } as Partial<General<TriggerState>>, destGeneral.id));
|
||||
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${destGeneralName}</>에게 부대 탈퇴를 지시했습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
createLogEffect(`<Y>${destGeneralName}</>에게 부대 탈퇴를 지시했습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
})
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${general.name}</>에게 부대 탈퇴를 지시 받았습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
generalId: destGeneral.id,
|
||||
}
|
||||
)
|
||||
createLogEffect(`<Y>${general.name}</>에게 부대 탈퇴를 지시 받았습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
generalId: destGeneral.id,
|
||||
})
|
||||
);
|
||||
|
||||
return { effects };
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { 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';
|
||||
@@ -50,8 +47,7 @@ const parseMonth = (raw: unknown): number | null => {
|
||||
return month >= 1 && month <= 12 ? month : null;
|
||||
};
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number =>
|
||||
year * 12 + month - 1;
|
||||
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const requireMinimumTerm = (minMonths: number): Constraint => ({
|
||||
name: 'RequireNonAggressionMinimumTerm',
|
||||
@@ -65,8 +61,7 @@ const requireMinimumTerm = (minMonths: number): Constraint => ({
|
||||
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
||||
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
||||
const envMonthValue =
|
||||
typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const missing = [];
|
||||
|
||||
if (yearValue === null) {
|
||||
@@ -106,7 +101,7 @@ const requireMinimumTerm = (minMonths: number): Constraint => ({
|
||||
|
||||
// 불가침 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionProposalArgs> {
|
||||
public readonly key = 'che_불가침제의';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -126,10 +121,7 @@ export class ActionDefinition<
|
||||
return { destNationId, year, month };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NonAggressionProposalArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionProposalArgs): Constraint[] {
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
@@ -149,14 +141,11 @@ export class ActionDefinition<
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
@@ -35,10 +32,8 @@ const parseNationId = (raw: unknown): number | null => {
|
||||
|
||||
// 불가침 파기 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements
|
||||
GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs>
|
||||
{
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs> {
|
||||
public readonly key = 'che_불가침파기제의';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
@@ -51,20 +46,14 @@ export class ActionDefinition<
|
||||
return { destNationId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NonAggressionCancelProposalArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelProposalArgs): Constraint[] {
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
existsDestNation(),
|
||||
allowDiplomacyBetweenStatus(
|
||||
[DIPLOMACY_NON_AGGRESSION],
|
||||
'불가침 중인 상대국에게만 가능합니다.'
|
||||
),
|
||||
allowDiplomacyBetweenStatus([DIPLOMACY_NON_AGGRESSION], '불가침 중인 상대국에게만 가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -74,14 +63,11 @@ export class ActionDefinition<
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } 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 { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
@@ -36,7 +33,7 @@ const parseNationId = (raw: unknown): number | null => {
|
||||
};
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DeclareWarArgs> {
|
||||
public readonly key = 'che_선전포고';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -73,14 +70,11 @@ export class ActionDefinition<
|
||||
if (nationId === undefined || nationId <= 0) {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -94,14 +88,11 @@ export class ActionDefinition<
|
||||
state: DIPLOMACY_DECLARE,
|
||||
term: DECLARE_TERM,
|
||||
}),
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
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,
|
||||
@@ -13,10 +8,7 @@ import {
|
||||
notOccupiedDestCity,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -36,7 +28,7 @@ export interface FloodArgs {
|
||||
}
|
||||
|
||||
export interface FloodResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation: Nation | null;
|
||||
@@ -59,9 +51,7 @@ const parseCityId = (raw: unknown): number | null => {
|
||||
};
|
||||
|
||||
// 수몰 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
@@ -69,20 +59,13 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
getGlobalDelay(context: FloodResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
}
|
||||
|
||||
// 수몰 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, FloodArgs> {
|
||||
readonly key = 'che_수몰';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
@@ -91,10 +74,7 @@ export class ActionResolver<
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: FloodResolveContext<TriggerState>,
|
||||
_args: FloodArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: FloodResolveContext<TriggerState>, _args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
@@ -107,13 +87,10 @@ export class ActionResolver<
|
||||
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,
|
||||
}
|
||||
);
|
||||
context.addLog(`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
});
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
@@ -188,12 +165,8 @@ export class ActionResolver<
|
||||
|
||||
// 수몰 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
FloodArgs,
|
||||
FloodResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, FloodArgs, FloodResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_수몰';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -211,10 +184,7 @@ export class ActionDefinition<
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: FloodArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: FloodArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
@@ -227,10 +197,7 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: FloodResolveContext<TriggerState>,
|
||||
args: FloodArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: FloodResolveContext<TriggerState>, args: FloodArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -251,12 +218,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
}
|
||||
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
|
||||
);
|
||||
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
|
||||
const destNationGenerals = generals.filter((general) => general.nationId === destCity.nationId);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
@@ -271,6 +234,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
StatBlock,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState, StatBlock, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
availableStrategicCommand,
|
||||
beChief,
|
||||
@@ -15,10 +8,7 @@ import {
|
||||
notOpeningPart,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -26,9 +16,7 @@ import type {
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import {
|
||||
createGeneralAddEffect,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createGeneralAddEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import { buildRecruitmentGeneral } from '@sammo-ts/logic/actions/turn/general/recruitment.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
@@ -55,7 +43,7 @@ export interface VolunteerRecruitCandidate {
|
||||
}
|
||||
|
||||
export interface VolunteerRecruitResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
currentYear: number;
|
||||
startYear: number;
|
||||
@@ -83,10 +71,7 @@ export interface VolunteerRecruitEnvironment {
|
||||
killTurnMin?: number;
|
||||
killTurnMax?: number;
|
||||
decorateName?: (name: string, npcState: number) => string;
|
||||
pickCandidate?: (
|
||||
context: VolunteerRecruitResolveContext,
|
||||
rng: RandomGenerator
|
||||
) => VolunteerRecruitCandidate | null;
|
||||
pickCandidate?: (context: VolunteerRecruitResolveContext, rng: RandomGenerator) => VolunteerRecruitCandidate | null;
|
||||
buildStats?: (
|
||||
context: VolunteerRecruitResolveContext,
|
||||
rng: RandomGenerator,
|
||||
@@ -117,19 +102,12 @@ const addMetaValue = (
|
||||
meta[key] = value;
|
||||
};
|
||||
|
||||
const readMetaNumber = (
|
||||
meta: Record<string, TriggerValue>,
|
||||
key: string
|
||||
): number | null => {
|
||||
const readMetaNumber = (meta: Record<string, TriggerValue>, key: string): number | null => {
|
||||
const value = meta[key];
|
||||
return typeof value === 'number' ? value : null;
|
||||
};
|
||||
|
||||
const randomRangeInt = (
|
||||
rng: RandomGenerator,
|
||||
min: number,
|
||||
max: number
|
||||
): number => rng.nextInt(min, max + 1);
|
||||
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
|
||||
|
||||
const resolveRelYear = (ctx: ConstraintContext): number => {
|
||||
const relYear = ctx.env.relYear;
|
||||
@@ -173,8 +151,7 @@ const resolveStats = (
|
||||
if (env.buildStats) {
|
||||
return env.buildStats(context, rng, candidate);
|
||||
}
|
||||
const fallback =
|
||||
context.nationAverageStats ?? context.general.stats;
|
||||
const fallback = context.nationAverageStats ?? context.general.stats;
|
||||
return {
|
||||
leadership: candidate.stats?.leadership ?? fallback.leadership,
|
||||
strength: candidate.stats?.strength ?? fallback.strength,
|
||||
@@ -183,9 +160,7 @@ const resolveStats = (
|
||||
};
|
||||
|
||||
// 의병모집 쿨타임/인원 계산을 제공한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
private readonly env: VolunteerRecruitEnvironment;
|
||||
|
||||
@@ -197,34 +172,15 @@ export class CommandResolver<
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
getPostDelay(
|
||||
context: VolunteerRecruitResolveContext<TriggerState>,
|
||||
gennum: number
|
||||
): number {
|
||||
getPostDelay(context: VolunteerRecruitResolveContext<TriggerState>, gennum: number): number {
|
||||
const fitted = Math.max(gennum, this.env.initialNationGenLimit);
|
||||
const base = Math.round(Math.sqrt(fitted * 10) * 10);
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'delay',
|
||||
base
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
|
||||
}
|
||||
|
||||
getGlobalDelay(
|
||||
context: VolunteerRecruitResolveContext<TriggerState>
|
||||
): number {
|
||||
getGlobalDelay(context: VolunteerRecruitResolveContext<TriggerState>): number {
|
||||
const base = this.env.globalDelayBase ?? DEFAULT_GLOBAL_DELAY;
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
base
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', base));
|
||||
}
|
||||
|
||||
getCreateCount(avgNationGenCount: number): number {
|
||||
@@ -236,7 +192,7 @@ export class CommandResolver<
|
||||
|
||||
// 의병모집 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, VolunteerRecruitArgs> {
|
||||
readonly key = 'che_의병모집';
|
||||
private readonly env: VolunteerRecruitEnvironment;
|
||||
@@ -275,35 +231,24 @@ export class ActionResolver<
|
||||
const generalName = general.name;
|
||||
const generalJosa = JosaUtil.pick(generalName, '이');
|
||||
const actionJosa = JosaUtil.pick(ACTION_NAME, '을');
|
||||
context.addLog(
|
||||
`<Y>${generalName}</>${generalJosa} <M>${ACTION_NAME}</>${actionJosa} 발동했습니다.`,
|
||||
{
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
}
|
||||
);
|
||||
context.addLog(`<Y>${generalName}</>${generalJosa} <M>${ACTION_NAME}</>${actionJosa} 발동했습니다.`, {
|
||||
scope: LogScope.NATION,
|
||||
category: LogCategory.HISTORY,
|
||||
nationId: nation.id,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
});
|
||||
}
|
||||
|
||||
const avgNationGen =
|
||||
Number.isFinite(context.averageNationGeneralCount)
|
||||
? context.averageNationGeneralCount
|
||||
: 0;
|
||||
const createCount = Math.max(
|
||||
0,
|
||||
this.command.getCreateCount(avgNationGen)
|
||||
);
|
||||
const gennumValue = nation
|
||||
? readMetaNumber(nation.meta, 'gennum')
|
||||
: null;
|
||||
const avgNationGen = Number.isFinite(context.averageNationGeneralCount) ? context.averageNationGeneralCount : 0;
|
||||
const createCount = Math.max(0, this.command.getCreateCount(avgNationGen));
|
||||
const gennumValue = nation ? readMetaNumber(nation.meta, 'gennum') : null;
|
||||
const currentGennum = gennumValue ?? 0;
|
||||
const nextGennum = currentGennum + createCount;
|
||||
const globalDelay = this.command.getGlobalDelay(context);
|
||||
|
||||
if (nation) {
|
||||
nation.meta = {
|
||||
...nation.meta as object,
|
||||
...(nation.meta as object),
|
||||
gennum: nextGennum,
|
||||
strategic_cmd_limit: globalDelay,
|
||||
};
|
||||
@@ -318,20 +263,11 @@ export class ActionResolver<
|
||||
|
||||
for (let idx = 0; idx < createCount; idx += 1) {
|
||||
const newGeneralId = context.createGeneralId();
|
||||
const candidate =
|
||||
resolveCandidate(context, context.rng, this.env) ??
|
||||
{ name: `NPC_${newGeneralId}` };
|
||||
const name = this.env.decorateName
|
||||
? this.env.decorateName(candidate.name, NPC_TYPE)
|
||||
: candidate.name;
|
||||
const candidate = resolveCandidate(context, context.rng, this.env) ?? { name: `NPC_${newGeneralId}` };
|
||||
const name = this.env.decorateName ? this.env.decorateName(candidate.name, NPC_TYPE) : candidate.name;
|
||||
const birthYear = context.currentYear - baseAge;
|
||||
const deathYear = context.currentYear + deathYears;
|
||||
const stats = resolveStats(
|
||||
context,
|
||||
context.rng,
|
||||
this.env,
|
||||
candidate
|
||||
);
|
||||
const stats = resolveStats(context, context.rng, this.env, candidate);
|
||||
const meta: Record<string, TriggerValue> = {
|
||||
npcType: NPC_TYPE,
|
||||
crewTypeId: this.env.defaultCrewTypeId,
|
||||
@@ -342,11 +278,7 @@ export class ActionResolver<
|
||||
addMetaValue(meta, 'deathYear', deathYear);
|
||||
addMetaValue(meta, 'specAge', DEFAULT_SPEC_AGE);
|
||||
addMetaValue(meta, 'specAge2', DEFAULT_SPEC_AGE);
|
||||
addMetaValue(
|
||||
meta,
|
||||
'killturn',
|
||||
randomRangeInt(context.rng, killTurnMin, killTurnMax)
|
||||
);
|
||||
addMetaValue(meta, 'killturn', randomRangeInt(context.rng, killTurnMin, killTurnMax));
|
||||
addMetaValue(meta, 'text', candidate.text ?? null);
|
||||
|
||||
const newGeneral = buildRecruitmentGeneral<TriggerState>({
|
||||
@@ -378,12 +310,8 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
VolunteerRecruitArgs,
|
||||
VolunteerRecruitResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, VolunteerRecruitArgs, VolunteerRecruitResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_의병모집';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -402,10 +330,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
_args: VolunteerRecruitArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(ctx: ConstraintContext, _args: VolunteerRecruitArgs): Constraint[] {
|
||||
void _args;
|
||||
const relYear = resolveRelYear(ctx);
|
||||
return [
|
||||
@@ -427,17 +352,12 @@ export class ActionDefinition<
|
||||
|
||||
// 예약 턴 실행에 필요한 국가 평균 정보를 구성한다.
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
const nationSummary = buildNationSummary(
|
||||
options.worldRef,
|
||||
base.general.nationId
|
||||
);
|
||||
const nationSummary = buildNationSummary(options.worldRef, base.general.nationId);
|
||||
return {
|
||||
...base,
|
||||
currentYear: options.world.currentYear,
|
||||
startYear: resolveStartYear(options.world, options.scenarioMeta),
|
||||
averageNationGeneralCount: buildAverageNationGeneralCount(
|
||||
options.worldRef
|
||||
),
|
||||
averageNationGeneralCount: buildAverageNationGeneralCount(options.worldRef),
|
||||
nationAverageStats: nationSummary.averageStats,
|
||||
nationAverageExperience: nationSummary.averageExperience,
|
||||
nationAverageDedication: nationSummary.averageDedication,
|
||||
@@ -450,6 +370,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
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,
|
||||
@@ -11,10 +7,7 @@ import {
|
||||
existsDestNation,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -22,17 +15,11 @@ import type {
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} 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 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 { buildDefaultDiplomacy, DIPLOMACY_STATE } from '../../../diplomacy/index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
|
||||
@@ -41,7 +28,7 @@ export interface DegradeRelationsArgs {
|
||||
}
|
||||
|
||||
export interface DegradeRelationsResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destNation: Nation;
|
||||
diplomacy: { state: number; term: number };
|
||||
@@ -63,13 +50,10 @@ const parseNationId = (raw: unknown): number | null => {
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const resolveNextTerm = (state: number, term: number): number =>
|
||||
state === DIPLOMACY_STATE.WAR ? 3 : term + 3;
|
||||
const resolveNextTerm = (state: number, term: number): number => (state === DIPLOMACY_STATE.WAR ? 3 : term + 3);
|
||||
|
||||
// 이호경식 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
@@ -77,20 +61,13 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DegradeRelationsResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
}
|
||||
|
||||
// 이호경식 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, DegradeRelationsArgs> {
|
||||
readonly key = 'che_이호경식';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
@@ -117,37 +94,20 @@ export class ActionResolver<
|
||||
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,
|
||||
}
|
||||
);
|
||||
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
|
||||
),
|
||||
}
|
||||
),
|
||||
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) {
|
||||
@@ -209,12 +169,8 @@ export class ActionResolver<
|
||||
|
||||
// 이호경식 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DegradeRelationsArgs,
|
||||
DegradeRelationsResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DegradeRelationsArgs, DegradeRelationsResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_이호경식';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -232,20 +188,14 @@ export class ActionDefinition<
|
||||
return { destNationId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DegradeRelationsArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DegradeRelationsArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
existsDestNation(),
|
||||
allowDiplomacyBetweenStatus(
|
||||
[0, 1],
|
||||
'선포, 전쟁중인 상대국에게만 가능합니다.'
|
||||
),
|
||||
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
@@ -279,12 +229,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
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
|
||||
);
|
||||
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
|
||||
const destNationGenerals = generals.filter((general) => general.nationId === destNationId);
|
||||
return {
|
||||
...base,
|
||||
destNation,
|
||||
@@ -300,6 +246,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
beChief,
|
||||
@@ -27,11 +18,7 @@ import type {
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionResolver,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import {
|
||||
createGeneralPatchEffect,
|
||||
createLogEffect,
|
||||
createNationPatchEffect,
|
||||
} from '@sammo-ts/logic/actions/engine.js';
|
||||
import { createGeneralPatchEffect, createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
@@ -46,7 +33,7 @@ export interface AwardArgs {
|
||||
}
|
||||
|
||||
export interface AwardResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destGeneral: General<TriggerState>;
|
||||
}
|
||||
@@ -63,16 +50,11 @@ const ACTION_NAME = '포상';
|
||||
const DEFAULT_MIN_AMOUNT = 100;
|
||||
const DEFAULT_AMOUNT_UNIT = 100;
|
||||
|
||||
const roundToUnit = (value: number, unit: number): number =>
|
||||
Math.round(value / unit) * unit;
|
||||
const roundToUnit = (value: number, unit: number): number => Math.round(value / unit) * unit;
|
||||
|
||||
const formatNumber = (value: number): string =>
|
||||
value.toLocaleString('en-US');
|
||||
const formatNumber = (value: number): string => value.toLocaleString('en-US');
|
||||
|
||||
const normalizeAmount = (
|
||||
amount: number,
|
||||
env: AwardEnvironment
|
||||
): number => {
|
||||
const normalizeAmount = (amount: number, env: AwardEnvironment): number => {
|
||||
const unit = env.amountUnit ?? DEFAULT_AMOUNT_UNIT;
|
||||
const min = env.minAmount ?? DEFAULT_MIN_AMOUNT;
|
||||
const max = env.maxAmount;
|
||||
@@ -108,7 +90,7 @@ export class CommandResolver {
|
||||
|
||||
// 포상 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, AwardArgs> {
|
||||
readonly key = 'che_포상';
|
||||
private readonly env: AwardEnvironment;
|
||||
@@ -119,10 +101,7 @@ export class ActionResolver<
|
||||
this.command = new CommandResolver(env);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AwardResolveContext<TriggerState>,
|
||||
args: AwardArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: AwardResolveContext<TriggerState>, args: AwardArgs): GeneralActionOutcome<TriggerState> {
|
||||
const nation = context.nation;
|
||||
if (!nation) {
|
||||
return { effects: [] };
|
||||
@@ -130,11 +109,7 @@ export class ActionResolver<
|
||||
const { key, label } = resolveNationResource(nation, args.isGold);
|
||||
const base = args.isGold ? this.env.baseGold : this.env.baseRice;
|
||||
const available = Math.max(nation[key] - base, 0);
|
||||
const amount = clamp(
|
||||
this.command.normalizeAmount(args.amount),
|
||||
0,
|
||||
available
|
||||
);
|
||||
const amount = clamp(this.command.normalizeAmount(args.amount), 0, available);
|
||||
if (amount <= 0) {
|
||||
return { effects: [] };
|
||||
}
|
||||
@@ -142,37 +117,32 @@ export class ActionResolver<
|
||||
const amountText = formatNumber(amount);
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createGeneralPatchEffect(
|
||||
{ [key]: context.destGeneral[key] + amount } as Partial<
|
||||
General<TriggerState>
|
||||
>,
|
||||
{ [key]: context.destGeneral[key] + amount } as Partial<General<TriggerState>>,
|
||||
context.destGeneral.id
|
||||
),
|
||||
createNationPatchEffect({
|
||||
[key]: nation[key] - amount,
|
||||
} as Partial<Nation>, nation.id),
|
||||
createNationPatchEffect(
|
||||
{
|
||||
[key]: nation[key] - amount,
|
||||
} as Partial<Nation>,
|
||||
nation.id
|
||||
),
|
||||
];
|
||||
|
||||
const amountJosa = JosaUtil.pick(amountText, '을');
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`${label} ${amountText}${amountJosa} 포상으로 받았습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: context.destGeneral.id,
|
||||
format: LogFormat.PLAIN,
|
||||
}
|
||||
)
|
||||
createLogEffect(`${label} ${amountText}${amountJosa} 포상으로 받았습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: context.destGeneral.id,
|
||||
format: LogFormat.PLAIN,
|
||||
})
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText}${amountJosa} 수여했습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
createLogEffect(`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText}${amountJosa} 수여했습니다.`, {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
})
|
||||
);
|
||||
|
||||
return { effects };
|
||||
@@ -180,12 +150,8 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
AwardArgs,
|
||||
AwardResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, AwardArgs, AwardResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_포상';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly command: CommandResolver;
|
||||
@@ -225,10 +191,7 @@ export class ActionDefinition<
|
||||
};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
args: AwardArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(ctx: ConstraintContext, args: AwardArgs): Constraint[] {
|
||||
const requirements: RequirementKey[] = [];
|
||||
if (ctx.cityId !== undefined) {
|
||||
requirements.push({ kind: 'city', id: ctx.cityId });
|
||||
@@ -262,10 +225,7 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AwardResolveContext<TriggerState>,
|
||||
args: AwardArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: AwardResolveContext<TriggerState>, args: AwardArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -293,9 +253,7 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
args: { isGold: true, amount: 1, destGeneralId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) => {
|
||||
const maxAmount =
|
||||
env.maxResourceActionAmount > 0
|
||||
? env.maxResourceActionAmount
|
||||
: Math.max(env.baseGold, env.baseRice, 1000);
|
||||
env.maxResourceActionAmount > 0 ? env.maxResourceActionAmount : Math.max(env.baseGold, env.baseRice, 1000);
|
||||
return new ActionDefinition({
|
||||
baseGold: env.baseGold,
|
||||
baseRice: env.baseRice,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allowDiplomacyStatus,
|
||||
@@ -9,10 +6,7 @@ import {
|
||||
beChief,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -30,7 +24,7 @@ import type { NationTurnCommandSpec } from './index.js';
|
||||
export interface DesperateFightArgs {}
|
||||
|
||||
export interface DesperateFightResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
nationGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
@@ -43,9 +37,7 @@ const TRAIN_CAP = 100;
|
||||
const ATMOS_CAP = 100;
|
||||
|
||||
// 필사즉생 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
@@ -53,20 +45,13 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DesperateFightResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
}
|
||||
|
||||
// 필사즉생 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, DesperateFightArgs> {
|
||||
readonly key = 'che_필사즉생';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
@@ -96,9 +81,7 @@ export class ActionResolver<
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
const updateTrainAtmos = (
|
||||
target: General<TriggerState>
|
||||
): { train: number; atmos: number } | null => {
|
||||
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) {
|
||||
@@ -119,9 +102,7 @@ export class ActionResolver<
|
||||
}
|
||||
const patch = updateTrainAtmos(target);
|
||||
if (patch) {
|
||||
effects.push(
|
||||
createGeneralPatchEffect(patch, target.id)
|
||||
);
|
||||
effects.push(createGeneralPatchEffect(patch, target.id));
|
||||
}
|
||||
effects.push(
|
||||
createLogEffect(broadcastMessage, {
|
||||
@@ -155,12 +136,8 @@ export class ActionResolver<
|
||||
|
||||
// 필사즉생 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
DesperateFightArgs,
|
||||
DesperateFightResolveContext<TriggerState>
|
||||
> {
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DesperateFightArgs, DesperateFightResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_필사즉생';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
@@ -174,10 +151,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DesperateFightArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DesperateFightArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
@@ -202,9 +176,7 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
if (!worldRef) {
|
||||
return null;
|
||||
}
|
||||
const nationGenerals = worldRef
|
||||
.listGenerals()
|
||||
.filter((entry) => entry.nationId === base.general.nationId);
|
||||
const nationGenerals = worldRef.listGenerals().filter((entry) => entry.nationId === base.general.nationId);
|
||||
return {
|
||||
...base,
|
||||
nationGenerals,
|
||||
@@ -216,6 +188,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
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,
|
||||
@@ -13,10 +8,7 @@ import {
|
||||
notOccupiedDestCity,
|
||||
occupiedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '@sammo-ts/logic/triggers/general-action.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,
|
||||
@@ -36,7 +28,7 @@ export interface DeceptionArgs {
|
||||
}
|
||||
|
||||
export interface DeceptionResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation: Nation | null;
|
||||
@@ -58,11 +50,7 @@ const parseCityId = (raw: unknown): number | null => {
|
||||
return value > 0 ? value : null;
|
||||
};
|
||||
|
||||
const pickMoveCityId = (
|
||||
rng: GeneralActionResolveContext['rng'],
|
||||
destCityId: number,
|
||||
candidates: City[]
|
||||
): number => {
|
||||
const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: number, candidates: City[]): number => {
|
||||
if (candidates.length === 0) {
|
||||
return destCityId;
|
||||
}
|
||||
@@ -76,9 +64,7 @@ const pickMoveCityId = (
|
||||
};
|
||||
|
||||
// 허보 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
@@ -86,20 +72,13 @@ export class CommandResolver<
|
||||
}
|
||||
|
||||
getGlobalDelay(context: DeceptionResolveContext<TriggerState>): number {
|
||||
return Math.round(
|
||||
this.pipeline.onCalcStrategic(
|
||||
context,
|
||||
ACTION_NAME,
|
||||
'globalDelay',
|
||||
DEFAULT_GLOBAL_DELAY
|
||||
)
|
||||
);
|
||||
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
|
||||
}
|
||||
}
|
||||
|
||||
// 허보 실행 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, DeceptionArgs> {
|
||||
readonly key = 'che_허보';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
@@ -108,10 +87,7 @@ export class ActionResolver<
|
||||
this.command = new CommandResolver(modules);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DeceptionResolveContext<TriggerState>,
|
||||
_args: DeceptionArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: DeceptionResolveContext<TriggerState>, _args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const { general, nation } = context;
|
||||
const generalName = general.name;
|
||||
@@ -124,13 +100,10 @@ export class ActionResolver<
|
||||
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,
|
||||
}
|
||||
);
|
||||
context.addLog(`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동`, {
|
||||
category: LogCategory.HISTORY,
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
});
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||
|
||||
@@ -149,11 +122,7 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
for (const target of context.destCityGenerals) {
|
||||
const moveCityId = pickMoveCityId(
|
||||
context.rng,
|
||||
context.destCity.id,
|
||||
context.destNationSupplyCities
|
||||
);
|
||||
const moveCityId = pickMoveCityId(context.rng, context.destCity.id, context.destNationSupplyCities);
|
||||
effects.push(
|
||||
createLogEffect(destBroadcastMessage, {
|
||||
scope: LogScope.GENERAL,
|
||||
@@ -163,12 +132,7 @@ export class ActionResolver<
|
||||
})
|
||||
);
|
||||
if (moveCityId !== target.cityId) {
|
||||
effects.push(
|
||||
createGeneralPatchEffect(
|
||||
{ cityId: moveCityId },
|
||||
target.id
|
||||
)
|
||||
);
|
||||
effects.push(createGeneralPatchEffect({ cityId: moveCityId }, target.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +172,7 @@ export class ActionResolver<
|
||||
|
||||
// 허보 실행을 위한 정의/제약을 구성한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, DeceptionArgs, DeceptionResolveContext<TriggerState>> {
|
||||
public readonly key = 'che_허보';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -227,10 +191,7 @@ export class ActionDefinition<
|
||||
return { destCityId };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: DeceptionArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: DeceptionArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [
|
||||
@@ -238,18 +199,12 @@ export class ActionDefinition<
|
||||
beChief(),
|
||||
notNeutralDestCity(),
|
||||
notOccupiedDestCity(),
|
||||
allowDiplomacyBetweenStatus(
|
||||
[0, 1],
|
||||
'선포, 전쟁중인 상대국에게만 가능합니다.'
|
||||
),
|
||||
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: DeceptionResolveContext<TriggerState>,
|
||||
args: DeceptionArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
resolve(context: DeceptionResolveContext<TriggerState>, args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -271,19 +226,12 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
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
|
||||
(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
|
||||
);
|
||||
.filter((city) => city.nationId === destCity.nationId && city.supplyState > 0);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
@@ -299,6 +247,5 @@ export const commandSpec: NationTurnCommandSpec = {
|
||||
category: '전략',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.generalActionModules ?? []),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -17,21 +17,15 @@ export const NATION_TURN_COMMAND_KEYS = [
|
||||
'che_급습',
|
||||
] as const;
|
||||
|
||||
export type NationTurnCommandKey =
|
||||
(typeof NATION_TURN_COMMAND_KEYS)[number];
|
||||
export type NationTurnCommandKey = (typeof NATION_TURN_COMMAND_KEYS)[number];
|
||||
|
||||
export type NationTurnCommandSpec =
|
||||
TurnCommandSpecBase<NationTurnCommandKey>;
|
||||
export type NationTurnCommandSpec = TurnCommandSpecBase<NationTurnCommandKey>;
|
||||
|
||||
export type NationTurnCommandModule =
|
||||
TurnCommandModule<NationTurnCommandSpec>;
|
||||
export type NationTurnCommandModule = TurnCommandModule<NationTurnCommandSpec>;
|
||||
|
||||
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
|
||||
|
||||
const defaultImporters: Record<
|
||||
NationTurnCommandKey,
|
||||
NationTurnCommandImporter
|
||||
> = {
|
||||
const defaultImporters: Record<NationTurnCommandKey, NationTurnCommandImporter> = {
|
||||
휴식: async () => import('./휴식.js'),
|
||||
che_포상: async () => import('./che_포상.js'),
|
||||
che_부대탈퇴지시: async () => import('./che_부대탈퇴지시.js'),
|
||||
@@ -48,29 +42,17 @@ const defaultImporters: Record<
|
||||
che_급습: async () => import('./che_급습.js'),
|
||||
};
|
||||
|
||||
export const isNationTurnCommandKey = (
|
||||
value: string
|
||||
): value is NationTurnCommandKey =>
|
||||
export const isNationTurnCommandKey = (value: string): value is NationTurnCommandKey =>
|
||||
NATION_TURN_COMMAND_KEYS.includes(value as NationTurnCommandKey);
|
||||
|
||||
|
||||
|
||||
export class NationTurnCommandLoader {
|
||||
private readonly cache = new Map<
|
||||
NationTurnCommandKey,
|
||||
Promise<NationTurnCommandModule>
|
||||
>();
|
||||
private readonly cache = new Map<NationTurnCommandKey, Promise<NationTurnCommandModule>>();
|
||||
|
||||
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);
|
||||
if (cached) {
|
||||
return cached;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
@@ -20,7 +15,7 @@ export interface NationRestArgs {}
|
||||
const ACTION_NAME = '휴식';
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, NationRestArgs> {
|
||||
readonly key = '휴식';
|
||||
|
||||
@@ -35,7 +30,7 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionDefinition<TriggerState, NationRestArgs> {
|
||||
public readonly key = '휴식';
|
||||
public readonly name = ACTION_NAME;
|
||||
@@ -46,10 +41,7 @@ export class ActionDefinition<
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NationRestArgs
|
||||
): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: NationRestArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [];
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
} from './helpers.js';
|
||||
import type { Constraint, RequirementKey } from './types.js';
|
||||
|
||||
export const occupiedCity = (
|
||||
options: { allowNeutral?: boolean } = {}
|
||||
): Constraint => ({
|
||||
export const occupiedCity = (options: { allowNeutral?: boolean } = {}): Constraint => ({
|
||||
name: 'OccupiedCity',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
@@ -93,8 +91,7 @@ export const occupiedDestCity = (): Constraint => ({
|
||||
|
||||
export const suppliedCity = (): Constraint => ({
|
||||
name: 'SuppliedCity',
|
||||
requires: (ctx) =>
|
||||
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
@@ -142,13 +139,9 @@ export const suppliedDestCity = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const remainCityCapacity = (
|
||||
key: keyof City,
|
||||
label: string
|
||||
): Constraint => ({
|
||||
export const remainCityCapacity = (key: keyof City, label: string): Constraint => ({
|
||||
name: 'RemainCityCapacity',
|
||||
requires: (ctx) =>
|
||||
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
@@ -171,14 +164,9 @@ export const remainCityCapacity = (
|
||||
},
|
||||
});
|
||||
|
||||
export const remainCityCapacityByMax = (
|
||||
key: keyof City,
|
||||
maxKey: keyof City,
|
||||
label: string
|
||||
): Constraint => ({
|
||||
export const remainCityCapacityByMax = (key: keyof City, maxKey: keyof City, label: string): Constraint => ({
|
||||
name: 'RemainCityCapacityByMax',
|
||||
requires: (ctx) =>
|
||||
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
@@ -200,14 +188,9 @@ export const remainCityCapacityByMax = (
|
||||
},
|
||||
});
|
||||
|
||||
export const reqCityCapacity = (
|
||||
key: keyof City,
|
||||
label: string,
|
||||
required: number | string
|
||||
): Constraint => ({
|
||||
export const reqCityCapacity = (key: keyof City, label: string, required: number | string): Constraint => ({
|
||||
name: 'ReqCityCapacity',
|
||||
requires: (ctx) =>
|
||||
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
@@ -240,8 +223,7 @@ export const reqCityCapacity = (
|
||||
|
||||
export const reqCityTrust = (minTrust: number): Constraint => ({
|
||||
name: 'ReqCityTrust',
|
||||
requires: (ctx) =>
|
||||
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
@@ -251,11 +233,7 @@ export const reqCityTrust = (minTrust: number): Constraint => ({
|
||||
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||
}
|
||||
const trust =
|
||||
readMetaNumberFromUnknown(
|
||||
city.meta,
|
||||
'trust'
|
||||
) ?? null;
|
||||
const trust = readMetaNumberFromUnknown(city.meta, 'trust') ?? null;
|
||||
if (trust === null) {
|
||||
return unknownOrDeny(ctx, [], '민심 정보가 없습니다.');
|
||||
}
|
||||
@@ -440,9 +418,7 @@ const hasRouteToDest = (
|
||||
export const hasRouteWithEnemy = (): Constraint => ({
|
||||
name: 'HasRouteWithEnemy',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [
|
||||
{ kind: 'general', id: ctx.actorId },
|
||||
];
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
if (destCityId !== undefined) {
|
||||
reqs.push({ kind: 'destCity', id: destCityId });
|
||||
@@ -469,9 +445,7 @@ export const hasRouteWithEnemy = (): Constraint => ({
|
||||
}
|
||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | null;
|
||||
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | null;
|
||||
const nations = view.get({ kind: 'env', key: 'nations' }) as
|
||||
| Array<{ id: number }>
|
||||
| null;
|
||||
const nations = view.get({ kind: 'env', key: 'nations' }) as Array<{ id: number }> | null;
|
||||
if (!map || !cities || !nations) {
|
||||
return unknownOrDeny(ctx, [], '경로 정보가 없습니다.');
|
||||
}
|
||||
@@ -480,11 +454,7 @@ export const hasRouteWithEnemy = (): Constraint => ({
|
||||
allowedNationIds.add(general.nationId);
|
||||
allowedNationIds.add(0);
|
||||
for (const nation of nations) {
|
||||
const state = readDiplomacyState(
|
||||
view,
|
||||
general.nationId,
|
||||
nation.id
|
||||
);
|
||||
const state = readDiplomacyState(view, general.nationId, nation.id);
|
||||
if (state === 0) {
|
||||
allowedNationIds.add(nation.id);
|
||||
}
|
||||
|
||||
@@ -32,17 +32,15 @@ const readDiplomacyEntry = (
|
||||
typeof record.state === 'number'
|
||||
? record.state
|
||||
: typeof record.stateCode === 'number'
|
||||
? record.stateCode
|
||||
: null;
|
||||
? 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 => ({
|
||||
export const disallowDiplomacyBetweenStatus = (disallowList: Record<number, string>): Constraint => ({
|
||||
name: 'DisallowDiplomacyBetweenStatus',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
@@ -73,8 +71,7 @@ export const disallowDiplomacyBetweenStatus = (
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const destCity = readDestCity(ctx, view);
|
||||
const destNationId =
|
||||
resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
const destNationId = resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
if (destNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
|
||||
}
|
||||
@@ -95,10 +92,7 @@ export const disallowDiplomacyBetweenStatus = (
|
||||
},
|
||||
});
|
||||
|
||||
export const allowDiplomacyBetweenStatus = (
|
||||
allowList: number[],
|
||||
reason: string
|
||||
): Constraint => ({
|
||||
export const allowDiplomacyBetweenStatus = (allowList: number[], reason: string): Constraint => ({
|
||||
name: 'AllowDiplomacyBetweenStatus',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
@@ -129,8 +123,7 @@ export const allowDiplomacyBetweenStatus = (
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const destCity = readDestCity(ctx, view);
|
||||
const destNationId =
|
||||
resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
const destNationId = resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
if (destNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
|
||||
}
|
||||
@@ -150,11 +143,7 @@ export const allowDiplomacyBetweenStatus = (
|
||||
},
|
||||
});
|
||||
|
||||
export const allowDiplomacyWithTerm = (
|
||||
requiredState: number,
|
||||
minTerm: number,
|
||||
reason: string
|
||||
): Constraint => ({
|
||||
export const allowDiplomacyWithTerm = (requiredState: number, minTerm: number, reason: string): Constraint => ({
|
||||
name: 'AllowDiplomacyWithTerm',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
@@ -185,8 +174,7 @@ export const allowDiplomacyWithTerm = (
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const destCity = readDestCity(ctx, view);
|
||||
const destNationId =
|
||||
resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
const destNationId = resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||
if (destNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
|
||||
}
|
||||
@@ -206,10 +194,7 @@ export const allowDiplomacyWithTerm = (
|
||||
},
|
||||
});
|
||||
|
||||
export const allowDiplomacyStatus = (
|
||||
allowList: number[],
|
||||
reason: string
|
||||
): Constraint => ({
|
||||
export const allowDiplomacyStatus = (allowList: number[], reason: string): Constraint => ({
|
||||
name: 'AllowDiplomacyStatus',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
ConstraintResult,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
} from './types.js';
|
||||
import type { Constraint, ConstraintContext, ConstraintResult, RequirementKey, StateView } from './types.js';
|
||||
|
||||
export const evaluateConstraints = (
|
||||
constraints: Constraint[],
|
||||
@@ -12,9 +6,7 @@ export const evaluateConstraints = (
|
||||
view: StateView
|
||||
): ConstraintResult => {
|
||||
for (const constraint of constraints) {
|
||||
const missing = constraint
|
||||
.requires(ctx)
|
||||
.filter((req) => !view.has(req));
|
||||
const missing = constraint.requires(ctx).filter((req) => !view.has(req));
|
||||
if (missing.length > 0 && ctx.mode === 'precheck') {
|
||||
return { kind: 'unknown', missing };
|
||||
}
|
||||
@@ -26,10 +18,7 @@ export const evaluateConstraints = (
|
||||
return { kind: 'allow' };
|
||||
};
|
||||
|
||||
export const collectRequirements = (
|
||||
constraints: Constraint[],
|
||||
ctx: ConstraintContext
|
||||
): RequirementKey[] => {
|
||||
export const collectRequirements = (constraints: Constraint[], ctx: ConstraintContext): RequirementKey[] => {
|
||||
const keys: RequirementKey[] = [];
|
||||
for (const constraint of constraints) {
|
||||
keys.push(...constraint.requires(ctx));
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import {
|
||||
allow,
|
||||
readDestGeneral,
|
||||
resolveDestGeneralId,
|
||||
resolveDestNationId,
|
||||
unknownOrDeny,
|
||||
} from './helpers.js';
|
||||
import { allow, readDestGeneral, resolveDestGeneralId, resolveDestNationId, unknownOrDeny } from './helpers.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
|
||||
|
||||
export const notBeNeutral = (): Constraint => ({
|
||||
@@ -73,9 +67,7 @@ export const reqGeneralGold = (
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
||||
test: (ctx, view) => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
const missing = [generalReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
const missing = [generalReq, ...requirements].filter((req) => !view.has(req));
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
|
||||
}
|
||||
@@ -99,9 +91,7 @@ export const reqGeneralRice = (
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
||||
test: (ctx, view) => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
const missing = [generalReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
const missing = [generalReq, ...requirements].filter((req) => !view.has(req));
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
|
||||
}
|
||||
@@ -144,9 +134,7 @@ export const reqGeneralCrewMargin = (
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
||||
test: (ctx, view) => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
const missing = [generalReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
const missing = [generalReq, ...requirements].filter((req) => !view.has(req));
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
|
||||
}
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import type { City, General, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
ConstraintContext,
|
||||
ConstraintResult,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
} from './types.js';
|
||||
import type { ConstraintContext, ConstraintResult, RequirementKey, StateView } from './types.js';
|
||||
|
||||
export const allow = (): ConstraintResult => ({ kind: 'allow' });
|
||||
|
||||
export const unknownOrDeny = (
|
||||
ctx: ConstraintContext,
|
||||
missing: RequirementKey[],
|
||||
reason: string
|
||||
): ConstraintResult =>
|
||||
ctx.mode === 'precheck'
|
||||
? { kind: 'unknown', missing }
|
||||
: { kind: 'deny', reason };
|
||||
export const unknownOrDeny = (ctx: ConstraintContext, missing: RequirementKey[], reason: string): ConstraintResult =>
|
||||
ctx.mode === 'precheck' ? { kind: 'unknown', missing } : { kind: 'deny', reason };
|
||||
|
||||
export const readGeneral = (
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): General | null => {
|
||||
export const readGeneral = (ctx: ConstraintContext, view: StateView): General | null => {
|
||||
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
if (!view.has(req)) {
|
||||
return null;
|
||||
@@ -47,10 +33,7 @@ export const resolveDestGeneralId = (ctx: ConstraintContext): number | undefined
|
||||
return typeof raw === 'number' ? raw : undefined;
|
||||
};
|
||||
|
||||
export const readDestGeneral = (
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): General | null => {
|
||||
export const readDestGeneral = (ctx: ConstraintContext, view: StateView): General | null => {
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId === undefined) {
|
||||
return null;
|
||||
@@ -78,18 +61,12 @@ export const resolveDestNationId = (ctx: ConstraintContext): number | undefined
|
||||
return typeof raw === 'number' ? raw : undefined;
|
||||
};
|
||||
|
||||
export const readDestCity = (
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): City | null => {
|
||||
export const readDestCity = (ctx: ConstraintContext, view: StateView): City | null => {
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
return readCity(view, destCityId);
|
||||
};
|
||||
|
||||
export const readNation = (
|
||||
view: StateView,
|
||||
id?: number
|
||||
): Nation | null => {
|
||||
export const readNation = (view: StateView, id?: number): Nation | null => {
|
||||
if (id === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -100,19 +77,12 @@ export const readNation = (
|
||||
return view.get(req) as Nation | null;
|
||||
};
|
||||
|
||||
export const readMetaNumber = (
|
||||
meta: Record<string, TriggerValue>,
|
||||
key: string
|
||||
): number | null => {
|
||||
export const readMetaNumber = (meta: Record<string, TriggerValue>, key: string): number | null => {
|
||||
const value = meta[key];
|
||||
return typeof value === 'number' ? value : null;
|
||||
};
|
||||
|
||||
export const readDiplomacyState = (
|
||||
view: StateView,
|
||||
srcNationId: number,
|
||||
destNationId: number
|
||||
): number | null => {
|
||||
export const readDiplomacyState = (view: StateView, srcNationId: number, destNationId: number): number | null => {
|
||||
const req: RequirementKey = {
|
||||
kind: 'diplomacy',
|
||||
srcNationId,
|
||||
@@ -138,10 +108,7 @@ export const readDiplomacyState = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const readMetaNumberFromUnknown = (
|
||||
meta: Record<string, unknown>,
|
||||
key: string
|
||||
): number | null => {
|
||||
export const readMetaNumberFromUnknown = (meta: Record<string, unknown>, key: string): number | null => {
|
||||
const value = meta[key];
|
||||
return typeof value === 'number' ? value : null;
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ export const alwaysFail = (reason: string): Constraint => ({
|
||||
test: () => ({ kind: 'deny', reason }),
|
||||
});
|
||||
|
||||
export const notOpeningPart = (
|
||||
relYear: number,
|
||||
openingPartYear: number
|
||||
): Constraint => ({
|
||||
export const notOpeningPart = (relYear: number, openingPartYear: number): Constraint => ({
|
||||
name: 'NotOpeningPart',
|
||||
requires: () => [],
|
||||
test: (_ctx) => {
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import type { 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';
|
||||
|
||||
export const notWanderingNation = (): Constraint => ({
|
||||
name: 'NotWanderingNation',
|
||||
requires: (ctx) =>
|
||||
ctx.nationId !== undefined
|
||||
? [{ kind: 'nation', id: ctx.nationId }]
|
||||
: [],
|
||||
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const nation = readNation(view, ctx.nationId);
|
||||
if (!nation) {
|
||||
@@ -31,9 +21,7 @@ export const notWanderingNation = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const availableStrategicCommand = (
|
||||
allowTurnCnt = 0
|
||||
): Constraint => ({
|
||||
export const availableStrategicCommand = (allowTurnCnt = 0): Constraint => ({
|
||||
name: 'AvailableStrategicCommand',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
@@ -92,9 +80,7 @@ export const reqNationGold = (
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
|
||||
const missing = [nationReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
const missing = [nationReq, ...requirements].filter((req) => !view.has(req));
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '국가 정보가 없습니다.');
|
||||
}
|
||||
@@ -128,9 +114,7 @@ export const reqNationRice = (
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
|
||||
const missing = [nationReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
const missing = [nationReq, ...requirements].filter((req) => !view.has(req));
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '국가 정보가 없습니다.');
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ export const mustBeTroopLeader = (): Constraint => ({
|
||||
|
||||
export const reqTroopMembers = (): Constraint => ({
|
||||
name: 'ReqTroopMembers',
|
||||
requires: (ctx) => [
|
||||
{ kind: 'general', id: ctx.actorId },
|
||||
{ kind: 'generalList' },
|
||||
],
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, { kind: 'generalList' }],
|
||||
test: (ctx, view) => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
if (!view.has(generalReq)) {
|
||||
@@ -44,10 +41,7 @@ export const reqTroopMembers = (): Constraint => ({
|
||||
if (!generals) {
|
||||
return unknownOrDeny(ctx, [listReq], '장수 정보가 없습니다.');
|
||||
}
|
||||
const hasMember = generals.some(
|
||||
(entry) =>
|
||||
entry.troopId === general.troopId && entry.id !== general.id
|
||||
);
|
||||
const hasMember = generals.some((entry) => entry.troopId === general.troopId && entry.id !== general.id);
|
||||
if (hasMember) {
|
||||
return allow();
|
||||
}
|
||||
|
||||
@@ -31,15 +31,10 @@ export interface DiplomacyPatch {
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const buildDiplomacyKey = (
|
||||
srcNationId: number,
|
||||
destNationId: number
|
||||
): string => `${srcNationId}:${destNationId}`;
|
||||
export const buildDiplomacyKey = (srcNationId: number, destNationId: number): string =>
|
||||
`${srcNationId}:${destNationId}`;
|
||||
|
||||
export const buildDefaultDiplomacy = (
|
||||
srcNationId: number,
|
||||
destNationId: number
|
||||
): DiplomacyEntry => ({
|
||||
export const buildDefaultDiplomacy = (srcNationId: number, destNationId: number): DiplomacyEntry => ({
|
||||
fromNationId: srcNationId,
|
||||
toNationId: destNationId,
|
||||
state: DIPLOMACY_STATE.TRADE,
|
||||
@@ -48,16 +43,13 @@ export const buildDefaultDiplomacy = (
|
||||
meta: {},
|
||||
});
|
||||
|
||||
export const applyDiplomacyPatch = (
|
||||
entry: DiplomacyEntry,
|
||||
patch: DiplomacyPatch
|
||||
): DiplomacyEntry => {
|
||||
export const applyDiplomacyPatch = (entry: DiplomacyEntry, patch: DiplomacyPatch): DiplomacyEntry => {
|
||||
const nextDead =
|
||||
typeof patch.dead === 'number'
|
||||
? patch.dead
|
||||
: typeof patch.deadDelta === 'number'
|
||||
? entry.dead + patch.deadDelta
|
||||
: entry.dead;
|
||||
? entry.dead + patch.deadDelta
|
||||
: entry.dead;
|
||||
return {
|
||||
...entry,
|
||||
state: patch.state ?? entry.state,
|
||||
@@ -67,9 +59,7 @@ export const applyDiplomacyPatch = (
|
||||
};
|
||||
};
|
||||
|
||||
export const readDiplomacyMeta = (
|
||||
meta: Record<string, unknown>
|
||||
): { meta: Record<string, unknown>; dead: number } => {
|
||||
export const readDiplomacyMeta = (meta: Record<string, unknown>): { meta: Record<string, unknown>; dead: number } => {
|
||||
const rawDead = meta.dead;
|
||||
const dead = typeof rawDead === 'number' ? rawDead : 0;
|
||||
const cleaned = { ...meta };
|
||||
@@ -77,14 +67,11 @@ export const readDiplomacyMeta = (
|
||||
return { meta: cleaned, dead };
|
||||
};
|
||||
|
||||
export const buildDiplomacyMeta = (
|
||||
entry: DiplomacyEntry
|
||||
): Record<string, unknown> => ({
|
||||
export const buildDiplomacyMeta = (entry: DiplomacyEntry): Record<string, unknown> => ({
|
||||
...entry.meta,
|
||||
dead: entry.dead,
|
||||
});
|
||||
|
||||
|
||||
export const processDiplomacyMonth = (
|
||||
diplomacy: DiplomacyEntry[],
|
||||
generalCounts: Map<number, number>
|
||||
@@ -94,10 +81,7 @@ export const processDiplomacyMonth = (
|
||||
meta: { ...entry.meta },
|
||||
}));
|
||||
const byKey = new Map<string, DiplomacyEntry>(
|
||||
next.map((entry) => [
|
||||
buildDiplomacyKey(entry.fromNationId, entry.toNationId),
|
||||
entry,
|
||||
])
|
||||
next.map((entry) => [buildDiplomacyKey(entry.fromNationId, entry.toNationId), entry])
|
||||
);
|
||||
|
||||
// 전쟁 기간 갱신: 사상자에 따라 term 증가, 잔여 사상자 유지.
|
||||
@@ -127,14 +111,8 @@ export const processDiplomacyMonth = (
|
||||
if (processedPairs.has(pairKey)) {
|
||||
continue;
|
||||
}
|
||||
const opposite = byKey.get(
|
||||
buildDiplomacyKey(entry.toNationId, entry.fromNationId)
|
||||
);
|
||||
if (
|
||||
opposite &&
|
||||
opposite.state === DIPLOMACY_STATE.WAR &&
|
||||
opposite.term <= 1
|
||||
) {
|
||||
const opposite = byKey.get(buildDiplomacyKey(entry.toNationId, entry.fromNationId));
|
||||
if (opposite && opposite.state === DIPLOMACY_STATE.WAR && opposite.term <= 1) {
|
||||
entry.state = DIPLOMACY_STATE.TRADE;
|
||||
entry.term = 0;
|
||||
opposite.state = DIPLOMACY_STATE.TRADE;
|
||||
@@ -153,15 +131,9 @@ export const processDiplomacyMonth = (
|
||||
|
||||
// 불가침/선전포고 만료 처리.
|
||||
for (const entry of next) {
|
||||
if (
|
||||
entry.state === DIPLOMACY_STATE.NON_AGGRESSION &&
|
||||
entry.term === 0
|
||||
) {
|
||||
if (entry.state === DIPLOMACY_STATE.NON_AGGRESSION && entry.term === 0) {
|
||||
entry.state = DIPLOMACY_STATE.TRADE;
|
||||
} else if (
|
||||
entry.state === DIPLOMACY_STATE.DECLARATION &&
|
||||
entry.term === 0
|
||||
) {
|
||||
} else if (entry.state === DIPLOMACY_STATE.DECLARATION && entry.term === 0) {
|
||||
entry.state = DIPLOMACY_STATE.WAR;
|
||||
entry.term = DEFAULT_WAR_TERM;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,7 @@ export * from './diplomacy/index.js';
|
||||
export * from './logging/index.js';
|
||||
export * from './messages/index.js';
|
||||
export * from './items/index.js';
|
||||
export {
|
||||
ITEM_KEYS,
|
||||
createItemActionModules,
|
||||
createItemModuleRegistry,
|
||||
loadItemModules,
|
||||
} from './items/index.js';
|
||||
export { ITEM_KEYS, createItemActionModules, createItemModuleRegistry, loadItemModules } from './items/index.js';
|
||||
export * from './ports/world.js';
|
||||
export * from './ports/worldSnapshot.js';
|
||||
export * from './scenario/index.js';
|
||||
|
||||
@@ -29,9 +29,7 @@ export interface StatItemOptions {
|
||||
extraInfo?: string;
|
||||
}
|
||||
|
||||
export const createStatItemModule = (
|
||||
options: StatItemOptions
|
||||
): ItemModule => {
|
||||
export const createStatItemModule = (options: StatItemOptions): ItemModule => {
|
||||
const statLabel = resolveStatLabel(options.statName);
|
||||
const name = `${options.rawName}(+${options.statValue})`;
|
||||
const baseInfo = `${statLabel} +${options.statValue}`;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import {
|
||||
CheSnipingActivateTrigger,
|
||||
CheSnipingAttemptTrigger,
|
||||
} from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
import { CheSnipingActivateTrigger, CheSnipingAttemptTrigger } from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const raiseType =
|
||||
BaseWarUnitTrigger.TYPE_ITEM +
|
||||
BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 102;
|
||||
const raiseType = BaseWarUnitTrigger.TYPE_ITEM + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 102;
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_무기_02_단궁',
|
||||
@@ -29,13 +24,7 @@ export const itemModule: ItemModule = {
|
||||
return null;
|
||||
}
|
||||
return new WarTriggerCaller(
|
||||
new CheSnipingAttemptTrigger(
|
||||
context.unit,
|
||||
raiseType,
|
||||
0.01,
|
||||
10,
|
||||
30
|
||||
),
|
||||
new CheSnipingAttemptTrigger(context.unit, raiseType, 0.01, 10, 30),
|
||||
new CheSnipingActivateTrigger(context.unit, raiseType)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import {
|
||||
CheSnipingActivateTrigger,
|
||||
CheSnipingAttemptTrigger,
|
||||
} from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
import { CheSnipingActivateTrigger, CheSnipingAttemptTrigger } from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const raiseType =
|
||||
BaseWarUnitTrigger.TYPE_ITEM +
|
||||
BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 109;
|
||||
const raiseType = BaseWarUnitTrigger.TYPE_ITEM + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 109;
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_무기_09_동호비궁',
|
||||
@@ -30,14 +25,7 @@ export const itemModule: ItemModule = {
|
||||
return null;
|
||||
}
|
||||
return new WarTriggerCaller(
|
||||
new CheSnipingAttemptTrigger(
|
||||
context.unit,
|
||||
raiseType,
|
||||
0.2,
|
||||
20,
|
||||
40,
|
||||
20
|
||||
),
|
||||
new CheSnipingAttemptTrigger(context.unit, raiseType, 0.2, 20, 40, 20),
|
||||
new CheSnipingActivateTrigger(context.unit, raiseType)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,8 +10,7 @@ export const itemModule: ItemModule = {
|
||||
key: ITEM_KEY,
|
||||
rawName: '도기',
|
||||
name: '도기(보물)',
|
||||
info:
|
||||
'[개인] 판매 시 장수 소지금과 국고에 금, 쌀 중 하나를 추가 (총 +10,000, 2년마다 +5,000)',
|
||||
info: '[개인] 판매 시 장수 소지금과 국고에 금, 쌀 중 하나를 추가 (총 +10,000, 2년마다 +5,000)',
|
||||
slot: 'item',
|
||||
cost: 200,
|
||||
buyable: false,
|
||||
@@ -56,9 +55,7 @@ export const itemModule: ItemModule = {
|
||||
|
||||
const josa = JosaUtil.pick('도기', '을');
|
||||
context.log?.push(
|
||||
`<C>${itemModule.name}</>${josa} 판매하여 ${resName} <C>${score.toLocaleString(
|
||||
'en-US'
|
||||
)}</>을 보충합니다.`
|
||||
`<C>${itemModule.name}</>${josa} 판매하여 ${resName} <C>${score.toLocaleString('en-US')}</>을 보충합니다.`
|
||||
);
|
||||
return aux;
|
||||
},
|
||||
|
||||
@@ -18,8 +18,7 @@ export const itemModule: ItemModule = {
|
||||
unique: false,
|
||||
getPreTurnExecuteTriggerList: (context) => {
|
||||
const target = context.general.triggerState.meta['use_treatment'];
|
||||
const injuryTarget =
|
||||
typeof target === 'number' && Number.isFinite(target) ? target : 10;
|
||||
const injuryTarget = typeof target === 'number' && Number.isFinite(target) ? target : 10;
|
||||
return new GeneralTriggerCaller(
|
||||
new CheItemHealTrigger(context.general, {
|
||||
injuryTarget,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import {
|
||||
GeneralTriggerCaller,
|
||||
type GeneralActionContext,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import { GeneralTriggerCaller, type GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type {
|
||||
GeneralStatName,
|
||||
TriggerActionPhase,
|
||||
@@ -47,15 +44,12 @@ const defaultImporters: Record<ItemKey, ItemImporter> = {
|
||||
che_보물_도기: async () => import('./che_보물_도기.js'),
|
||||
};
|
||||
|
||||
export const isItemKey = (value: string): value is ItemKey =>
|
||||
ITEM_KEYS.includes(value as ItemKey);
|
||||
export const isItemKey = (value: string): value is ItemKey => ITEM_KEYS.includes(value as ItemKey);
|
||||
|
||||
export class ItemLoader {
|
||||
private readonly cache = new Map<ItemKey, Promise<ItemModule>>();
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<ItemKey, ItemImporter> = defaultImporters
|
||||
) {}
|
||||
constructor(private readonly importers: Record<ItemKey, ItemImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: ItemKey): Promise<ItemModule> {
|
||||
const cached = this.cache.get(key);
|
||||
@@ -72,9 +66,7 @@ export class ItemLoader {
|
||||
}
|
||||
const resolved = module.itemModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(
|
||||
`Item key mismatch: expected ${key}, got ${resolved.key}`
|
||||
);
|
||||
throw new Error(`Item key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
@@ -99,13 +91,12 @@ export const loadItemModules = async (
|
||||
return modules;
|
||||
};
|
||||
|
||||
export type ItemModuleRegistry<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> = Map<string, ItemModule<TriggerState>>;
|
||||
export type ItemModuleRegistry<TriggerState extends GeneralTriggerState = GeneralTriggerState> = Map<
|
||||
string,
|
||||
ItemModule<TriggerState>
|
||||
>;
|
||||
|
||||
export const createItemModuleRegistry = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const createItemModuleRegistry = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
modules: ItemModule<TriggerState>[]
|
||||
): ItemModuleRegistry<TriggerState> => {
|
||||
const registry: ItemModuleRegistry<TriggerState> = new Map();
|
||||
@@ -116,13 +107,11 @@ export const createItemModuleRegistry = <
|
||||
};
|
||||
|
||||
class ItemGeneralActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionModule<TriggerState> {
|
||||
constructor(private readonly registry: ItemModuleRegistry<TriggerState>) {}
|
||||
|
||||
private resolveModules(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
): Array<ItemModule<TriggerState>> {
|
||||
private resolveModules(context: GeneralActionContext<TriggerState>): Array<ItemModule<TriggerState>> {
|
||||
const keys = listEquippedItemKeys(context.general);
|
||||
const modules: Array<ItemModule<TriggerState>> = [];
|
||||
for (const key of keys) {
|
||||
@@ -248,13 +237,11 @@ class ItemGeneralActionRouter<
|
||||
}
|
||||
|
||||
class ItemWarActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements WarActionModule<TriggerState> {
|
||||
constructor(private readonly registry: ItemModuleRegistry<TriggerState>) {}
|
||||
|
||||
private resolveModules(
|
||||
context: WarActionContext<TriggerState>
|
||||
): Array<ItemModule<TriggerState>> {
|
||||
private resolveModules(context: WarActionContext<TriggerState>): Array<ItemModule<TriggerState>> {
|
||||
const keys = listEquippedItemKeys(context.general);
|
||||
const modules: Array<ItemModule<TriggerState>> = [];
|
||||
for (const key of keys) {
|
||||
@@ -266,9 +253,7 @@ class ItemWarActionRouter<
|
||||
return modules;
|
||||
}
|
||||
|
||||
getBattleInitTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null {
|
||||
getBattleInitTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
|
||||
const caller = new WarTriggerCaller();
|
||||
for (const module of this.resolveModules(context)) {
|
||||
const triggers = module.getBattleInitTriggerList?.(context);
|
||||
@@ -279,9 +264,7 @@ class ItemWarActionRouter<
|
||||
return caller.isEmpty() ? null : caller;
|
||||
}
|
||||
|
||||
getBattlePhaseTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null {
|
||||
getBattlePhaseTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
|
||||
const caller = new WarTriggerCaller();
|
||||
for (const module of this.resolveModules(context)) {
|
||||
const triggers = module.getBattlePhaseTriggerList?.(context);
|
||||
@@ -335,11 +318,7 @@ class ItemWarActionRouter<
|
||||
if (!module.getWarPowerMultiplier) {
|
||||
continue;
|
||||
}
|
||||
const [attMul, defMul] = module.getWarPowerMultiplier(
|
||||
context,
|
||||
unit,
|
||||
oppose
|
||||
);
|
||||
const [attMul, defMul] = module.getWarPowerMultiplier(context, unit, oppose);
|
||||
attack *= attMul;
|
||||
defence *= defMul;
|
||||
}
|
||||
@@ -347,9 +326,7 @@ class ItemWarActionRouter<
|
||||
}
|
||||
}
|
||||
|
||||
export const createItemActionModules = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const createItemActionModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
registry: ItemModuleRegistry<TriggerState>
|
||||
): { general: GeneralActionModule<TriggerState>[]; war: WarActionModule<TriggerState>[] } => ({
|
||||
general: [new ItemGeneralActionRouter(registry)],
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
GeneralActionContext,
|
||||
GeneralTriggerCaller,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralActionContext, GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type {
|
||||
GeneralStatName,
|
||||
TriggerActionPhase,
|
||||
@@ -20,9 +17,7 @@ import type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
|
||||
export type ItemSlot = 'horse' | 'weapon' | 'book' | 'item';
|
||||
|
||||
export interface ItemModule<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface ItemModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
key: string;
|
||||
name: string;
|
||||
rawName: string;
|
||||
@@ -47,12 +42,7 @@ export interface ItemModule<
|
||||
): number;
|
||||
|
||||
onCalcStat?: {
|
||||
(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number;
|
||||
(context: GeneralActionContext<TriggerState>, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: WarStatName,
|
||||
@@ -62,12 +52,7 @@ export interface ItemModule<
|
||||
};
|
||||
|
||||
onCalcOpposeStat?: {
|
||||
(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number;
|
||||
(context: GeneralActionContext<TriggerState>, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: WarStatName,
|
||||
@@ -96,13 +81,9 @@ export interface ItemModule<
|
||||
aux?: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null;
|
||||
|
||||
getBattleInitTriggerList?(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null;
|
||||
getBattleInitTriggerList?(context: WarActionContext<TriggerState>): WarTriggerCaller | null;
|
||||
|
||||
getBattlePhaseTriggerList?(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null;
|
||||
getBattlePhaseTriggerList?(context: WarActionContext<TriggerState>): WarTriggerCaller | null;
|
||||
|
||||
getWarPowerMultiplier?(
|
||||
context: WarActionContext<TriggerState>,
|
||||
@@ -111,8 +92,6 @@ export interface ItemModule<
|
||||
): [number, number];
|
||||
}
|
||||
|
||||
export interface ItemModuleExport<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface ItemModuleExport<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
itemModule: ItemModule<TriggerState>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const ITEM_REMAIN_PREFIX = 'itemRemain:';
|
||||
@@ -24,15 +21,11 @@ const toBoolean = (value: unknown): boolean => {
|
||||
export const isInventoryEnabled = (config: ScenarioConfig): boolean => {
|
||||
const constConfig = config.const ?? {};
|
||||
return toBoolean(
|
||||
constConfig['allowInventory'] ??
|
||||
constConfig['inventoryEnabled'] ??
|
||||
constConfig['enableInventory']
|
||||
constConfig['allowInventory'] ?? constConfig['inventoryEnabled'] ?? constConfig['enableInventory']
|
||||
);
|
||||
};
|
||||
|
||||
export const listEquippedItemKeys = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
export const listEquippedItemKeys = <TriggerState extends GeneralTriggerState>(
|
||||
general: General<TriggerState>
|
||||
): string[] => {
|
||||
const items = [
|
||||
@@ -53,21 +46,15 @@ export const listEquippedItemKeys = <
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getItemRemain = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
export const getItemRemain = <TriggerState extends GeneralTriggerState>(
|
||||
general: General<TriggerState>,
|
||||
itemKey: string
|
||||
): number | null => {
|
||||
const value = general.triggerState.counters[
|
||||
`${ITEM_REMAIN_PREFIX}${itemKey}`
|
||||
];
|
||||
const value = general.triggerState.counters[`${ITEM_REMAIN_PREFIX}${itemKey}`];
|
||||
return typeof value === 'number' && value > 0 ? value : null;
|
||||
};
|
||||
|
||||
export const setItemRemain = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
export const setItemRemain = <TriggerState extends GeneralTriggerState>(
|
||||
general: General<TriggerState>,
|
||||
itemKey: string,
|
||||
remain: number | null
|
||||
@@ -80,9 +67,7 @@ export const setItemRemain = <
|
||||
general.triggerState.counters[key] = remain;
|
||||
};
|
||||
|
||||
export const consumeItemRemain = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
export const consumeItemRemain = <TriggerState extends GeneralTriggerState>(
|
||||
general: General<TriggerState>,
|
||||
itemKey: string,
|
||||
fallbackRemain = 1
|
||||
@@ -96,9 +81,7 @@ export const consumeItemRemain = <
|
||||
return true;
|
||||
};
|
||||
|
||||
export const canAcquireItem = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(options: {
|
||||
export const canAcquireItem = <TriggerState extends GeneralTriggerState>(options: {
|
||||
general: General<TriggerState>;
|
||||
item: ItemModule;
|
||||
config: ScenarioConfig;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
LogCategory,
|
||||
type LogEntryDraft,
|
||||
LogFormat,
|
||||
LogScope,
|
||||
} from './types.js';
|
||||
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from './types.js';
|
||||
|
||||
export class ActionLogger {
|
||||
private readonly generalId: number | undefined;
|
||||
@@ -26,62 +21,42 @@ export class ActionLogger {
|
||||
return backup;
|
||||
}
|
||||
|
||||
public pushGeneralHistoryLog(
|
||||
text: string | string[],
|
||||
format: LogFormat = LogFormat.YEAR_MONTH
|
||||
): void {
|
||||
public pushGeneralHistoryLog(text: string | string[], format: LogFormat = LogFormat.YEAR_MONTH): void {
|
||||
this.pushBatch(text, (message) => ({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.HISTORY,
|
||||
text: message,
|
||||
...(this.generalId !== undefined
|
||||
? { generalId: this.generalId }
|
||||
: {}),
|
||||
...(this.generalId !== undefined ? { generalId: this.generalId } : {}),
|
||||
format,
|
||||
}));
|
||||
}
|
||||
|
||||
public pushGeneralActionLog(
|
||||
text: string | string[],
|
||||
format: LogFormat = LogFormat.MONTH
|
||||
): void {
|
||||
public pushGeneralActionLog(text: string | string[], format: LogFormat = LogFormat.MONTH): void {
|
||||
this.pushBatch(text, (message) => ({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
text: message,
|
||||
...(this.generalId !== undefined
|
||||
? { generalId: this.generalId }
|
||||
: {}),
|
||||
...(this.generalId !== undefined ? { generalId: this.generalId } : {}),
|
||||
format,
|
||||
}));
|
||||
}
|
||||
|
||||
public pushGeneralBattleResultLog(
|
||||
text: string | string[],
|
||||
format: LogFormat = LogFormat.RAWTEXT
|
||||
): void {
|
||||
public pushGeneralBattleResultLog(text: string | string[], format: LogFormat = LogFormat.RAWTEXT): void {
|
||||
this.pushBatch(text, (message) => ({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.BATTLE_BRIEF,
|
||||
text: message,
|
||||
...(this.generalId !== undefined
|
||||
? { generalId: this.generalId }
|
||||
: {}),
|
||||
...(this.generalId !== undefined ? { generalId: this.generalId } : {}),
|
||||
format,
|
||||
}));
|
||||
}
|
||||
|
||||
public pushGeneralBattleDetailLog(
|
||||
text: string | string[],
|
||||
format: LogFormat = LogFormat.PLAIN
|
||||
): void {
|
||||
public pushGeneralBattleDetailLog(text: string | string[], format: LogFormat = LogFormat.PLAIN): void {
|
||||
this.pushBatch(text, (message) => ({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.BATTLE_DETAIL,
|
||||
text: message,
|
||||
...(this.generalId !== undefined
|
||||
? { generalId: this.generalId }
|
||||
: {}),
|
||||
...(this.generalId !== undefined ? { generalId: this.generalId } : {}),
|
||||
format,
|
||||
}));
|
||||
}
|
||||
@@ -103,10 +78,7 @@ export class ActionLogger {
|
||||
}));
|
||||
}
|
||||
|
||||
public pushGlobalHistoryLog(
|
||||
text: string | string[],
|
||||
format: LogFormat = LogFormat.YEAR_MONTH
|
||||
): void {
|
||||
public pushGlobalHistoryLog(text: string | string[], format: LogFormat = LogFormat.YEAR_MONTH): void {
|
||||
this.pushBatch(text, (message) => ({
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.HISTORY,
|
||||
@@ -115,10 +87,7 @@ export class ActionLogger {
|
||||
}));
|
||||
}
|
||||
|
||||
public pushGlobalActionLog(
|
||||
text: string | string[],
|
||||
format: LogFormat = LogFormat.MONTH
|
||||
): void {
|
||||
public pushGlobalActionLog(text: string | string[], format: LogFormat = LogFormat.MONTH): void {
|
||||
this.pushBatch(text, (message) => ({
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.SUMMARY,
|
||||
@@ -127,10 +96,7 @@ export class ActionLogger {
|
||||
}));
|
||||
}
|
||||
|
||||
private pushBatch(
|
||||
text: string | string[],
|
||||
builder: (message: string) => LogEntryDraft
|
||||
): void {
|
||||
private pushBatch(text: string | string[], builder: (message: string) => LogEntryDraft): void {
|
||||
if (Array.isArray(text)) {
|
||||
for (const item of text) {
|
||||
if (item) {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { formatLogText } from './formatter.js';
|
||||
import {
|
||||
type LogContext,
|
||||
type LogEntryDraft,
|
||||
type LogEntryRecord,
|
||||
LogFormat,
|
||||
LogScope,
|
||||
} from './types.js';
|
||||
import { type LogContext, type LogEntryDraft, type LogEntryRecord, LogFormat, LogScope } from './types.js';
|
||||
|
||||
const shouldDropEntry = (entry: LogEntryDraft): boolean => {
|
||||
if (entry.scope === LogScope.GENERAL && !entry.generalId) {
|
||||
@@ -20,10 +14,7 @@ const shouldDropEntry = (entry: LogEntryDraft): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const finalizeLogEntry = (
|
||||
entry: LogEntryDraft,
|
||||
context: LogContext
|
||||
): LogEntryRecord | null => {
|
||||
export const finalizeLogEntry = (entry: LogEntryDraft, context: LogContext): LogEntryRecord | null => {
|
||||
if (shouldDropEntry(entry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { LogFormat } from './types.js';
|
||||
|
||||
// 로그 포맷은 기존 표시 규칙(<C>/<S>/<R> + 기호)을 그대로 유지한다.
|
||||
export const formatLogText = (
|
||||
text: string,
|
||||
format: LogFormat,
|
||||
year: number,
|
||||
month: number
|
||||
): string => {
|
||||
export const formatLogText = (text: string, format: LogFormat, year: number, month: number): string => {
|
||||
switch (format) {
|
||||
case LogFormat.RAWTEXT:
|
||||
return text;
|
||||
|
||||
@@ -16,8 +16,7 @@ export const LogCategory = {
|
||||
USER: 'USER',
|
||||
} as const;
|
||||
|
||||
export type LogCategory =
|
||||
(typeof LogCategory)[keyof typeof LogCategory];
|
||||
export type LogCategory = (typeof LogCategory)[keyof typeof LogCategory];
|
||||
|
||||
export interface LogEntryDraft {
|
||||
scope: LogScope;
|
||||
|
||||
@@ -45,8 +45,7 @@ export interface MessageStore {
|
||||
insertMessage(draft: MessageRecordDraft): Promise<number>;
|
||||
}
|
||||
|
||||
export const isValidMailbox = (mailbox: number): boolean =>
|
||||
mailbox > 0 && mailbox <= MESSAGE_MAILBOX_PUBLIC;
|
||||
export const isValidMailbox = (mailbox: number): boolean => mailbox > 0 && mailbox <= MESSAGE_MAILBOX_PUBLIC;
|
||||
|
||||
export const resolveReceiverMailbox = (draft: MessageDraft): number => {
|
||||
switch (draft.msgType) {
|
||||
@@ -65,9 +64,7 @@ export const resolveSenderMailbox = (draft: MessageDraft): number | null => {
|
||||
case 'public':
|
||||
return null;
|
||||
case 'private':
|
||||
return draft.src.generalId !== draft.dest.generalId
|
||||
? draft.src.generalId
|
||||
: null;
|
||||
return draft.src.generalId !== draft.dest.generalId ? draft.src.generalId : null;
|
||||
case 'national':
|
||||
return draft.src.nationId !== draft.dest.nationId
|
||||
? MESSAGE_MAILBOX_NATIONAL_BASE + draft.src.nationId
|
||||
@@ -77,10 +74,7 @@ export const resolveSenderMailbox = (draft: MessageDraft): number | null => {
|
||||
}
|
||||
};
|
||||
|
||||
const buildPayload = (
|
||||
draft: MessageDraft,
|
||||
optionOverride?: MessageOption | null
|
||||
): MessagePayload => ({
|
||||
const buildPayload = (draft: MessageDraft, optionOverride?: MessageOption | null): MessagePayload => ({
|
||||
src: draft.src,
|
||||
dest: draft.dest,
|
||||
text: draft.text,
|
||||
@@ -114,10 +108,7 @@ const buildRecord = (
|
||||
};
|
||||
};
|
||||
|
||||
const buildSenderOption = (
|
||||
draft: MessageDraft,
|
||||
receiverId: number
|
||||
): MessageOption => {
|
||||
const buildSenderOption = (draft: MessageDraft, receiverId: number): MessageOption => {
|
||||
const option = {
|
||||
...(draft.option ?? {}),
|
||||
receiverMessageID: receiverId,
|
||||
@@ -157,11 +148,7 @@ export const sendMessage = async (
|
||||
return { receiverId };
|
||||
}
|
||||
|
||||
const senderRecord = buildRecord(
|
||||
draft,
|
||||
senderMailbox,
|
||||
buildSenderOption(draft, receiverId)
|
||||
);
|
||||
const senderRecord = buildRecord(draft, senderMailbox, buildSenderOption(draft, receiverId));
|
||||
const senderId = await store.insertMessage(senderRecord);
|
||||
|
||||
return senderId ? { receiverId, senderId } : { receiverId };
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
// 도메인 로직이 DB에 직접 접근하지 않도록 하는 인터페이스.
|
||||
|
||||
import type {
|
||||
City,
|
||||
CityId,
|
||||
General,
|
||||
GeneralId,
|
||||
Nation,
|
||||
NationId,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, CityId, General, GeneralId, Nation, NationId } from '@sammo-ts/logic/domain/entities.js';
|
||||
|
||||
export interface GeneralRepository<GeneralType extends General = General> {
|
||||
getById(id: GeneralId): Promise<GeneralType | null>;
|
||||
@@ -30,7 +23,7 @@ export interface NationRepository<NationType extends Nation = Nation> {
|
||||
export interface WorldStateReader<
|
||||
GeneralType extends General = General,
|
||||
CityType extends City = City,
|
||||
NationType extends Nation = Nation
|
||||
NationType extends Nation = Nation,
|
||||
> {
|
||||
getGeneralById(id: GeneralId): Promise<GeneralType | null>;
|
||||
getCityById(id: CityId): Promise<CityType | null>;
|
||||
@@ -40,7 +33,7 @@ export interface WorldStateReader<
|
||||
export interface WorldStateWriter<
|
||||
GeneralType extends General = General,
|
||||
CityType extends City = City,
|
||||
NationType extends Nation = Nation
|
||||
NationType extends Nation = Nation,
|
||||
> {
|
||||
saveGeneral(general: GeneralType): Promise<void>;
|
||||
saveCity(city: CityType): Promise<void>;
|
||||
@@ -50,6 +43,6 @@ export interface WorldStateWriter<
|
||||
export interface WorldStateRepository<
|
||||
GeneralType extends General = General,
|
||||
CityType extends City = City,
|
||||
NationType extends Nation = Nation
|
||||
> extends WorldStateReader<GeneralType, CityType, NationType>,
|
||||
WorldStateWriter<GeneralType, CityType, NationType> {}
|
||||
NationType extends Nation = Nation,
|
||||
>
|
||||
extends WorldStateReader<GeneralType, CityType, NationType>, WorldStateWriter<GeneralType, CityType, NationType> {}
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface WorldStateSnapshotSource<
|
||||
GeneralType extends General = General,
|
||||
CityType extends City = City,
|
||||
NationType extends Nation = Nation,
|
||||
TroopType extends Troop = Troop
|
||||
TroopType extends Troop = Troop,
|
||||
> {
|
||||
listGenerals(): Promise<GeneralType[]>;
|
||||
listCities(): Promise<CityType[]>;
|
||||
|
||||
@@ -26,28 +26,20 @@ const FALLBACK_STAT: ScenarioStatBlock = {
|
||||
const isRecord = (value: unknown): value is UnknownRecord =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const toRecordOrUndefined = (value: unknown): UnknownRecord | undefined =>
|
||||
isRecord(value) ? value : undefined;
|
||||
const toRecordOrUndefined = (value: unknown): UnknownRecord | undefined => (isRecord(value) ? value : undefined);
|
||||
|
||||
const toArrayOrUndefined = (value: unknown): unknown[] | undefined =>
|
||||
Array.isArray(value) ? value : undefined;
|
||||
const toArrayOrUndefined = (value: unknown): unknown[] | undefined => (Array.isArray(value) ? value : undefined);
|
||||
|
||||
const asNumber = (value: unknown, fallback: number): number =>
|
||||
typeof value === 'number' ? value : fallback;
|
||||
const asNumber = (value: unknown, fallback: number): number => (typeof value === 'number' ? value : fallback);
|
||||
|
||||
const asString = (value: unknown, fallback: string): string =>
|
||||
typeof value === 'string' ? value : fallback;
|
||||
const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
||||
|
||||
const asNullableNumber = (value: unknown): number | null =>
|
||||
typeof value === 'number' ? value : null;
|
||||
const asNullableNumber = (value: unknown): number | null => (typeof value === 'number' ? value : null);
|
||||
|
||||
const asNullableString = (value: unknown): string | null =>
|
||||
typeof value === 'string' ? value : null;
|
||||
const asNullableString = (value: unknown): string | null => (typeof value === 'string' ? value : null);
|
||||
|
||||
const asStringArray = (value: unknown): string[] =>
|
||||
Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === 'string')
|
||||
: [];
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
|
||||
const zRecord = z.record(z.string(), z.unknown());
|
||||
const zUnknownArray = z.array(z.unknown());
|
||||
@@ -96,10 +88,7 @@ const zScenarioInput = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const parseScenarioStatBlock = (
|
||||
value: unknown,
|
||||
fallback: ScenarioStatBlock
|
||||
): ScenarioStatBlock => {
|
||||
const parseScenarioStatBlock = (value: unknown, fallback: ScenarioStatBlock): ScenarioStatBlock => {
|
||||
const data = isRecord(value) ? value : {};
|
||||
return {
|
||||
total: asNumber(data.total, fallback.total),
|
||||
@@ -112,17 +101,12 @@ const parseScenarioStatBlock = (
|
||||
};
|
||||
};
|
||||
|
||||
const parseScenarioEnvironment = (
|
||||
mapConfig: UnknownRecord,
|
||||
constConfig: UnknownRecord
|
||||
): ScenarioEnvironment => {
|
||||
const parseScenarioEnvironment = (mapConfig: UnknownRecord, constConfig: UnknownRecord): ScenarioEnvironment => {
|
||||
const merged = { ...mapConfig, ...constConfig };
|
||||
const mapName = asString(merged.mapName, 'che');
|
||||
const unitSet = asString(merged.unitSet, 'che');
|
||||
const scenarioEffect =
|
||||
typeof merged.scenarioEffect === 'string' || merged.scenarioEffect === null
|
||||
? merged.scenarioEffect
|
||||
: undefined;
|
||||
typeof merged.scenarioEffect === 'string' || merged.scenarioEffect === null ? merged.scenarioEffect : undefined;
|
||||
|
||||
const result: ScenarioEnvironment = { mapName, unitSet };
|
||||
if (scenarioEffect !== undefined) {
|
||||
@@ -136,17 +120,7 @@ const parseNationRow = (row: unknown, index: number): ScenarioNation => {
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Scenario nation row ${index} is not an array.`);
|
||||
}
|
||||
const [
|
||||
name,
|
||||
color,
|
||||
gold,
|
||||
rice,
|
||||
infoText,
|
||||
tech,
|
||||
type,
|
||||
level,
|
||||
cities,
|
||||
] = parsed.data;
|
||||
const [name, color, gold, rice, infoText, tech, type, level, cities] = parsed.data;
|
||||
|
||||
const nationName = asString(name, '');
|
||||
if (!nationName) {
|
||||
@@ -181,11 +155,7 @@ const parseDiplomacyRow = (row: unknown, index: number): ScenarioDiplomacy => {
|
||||
};
|
||||
};
|
||||
|
||||
const parseGeneralRow = (
|
||||
row: unknown,
|
||||
index: number,
|
||||
label: string
|
||||
): ScenarioGeneral => {
|
||||
const parseGeneralRow = (row: unknown, index: number, label: string): ScenarioGeneral => {
|
||||
const parsed = zUnknownArray.safeParse(row);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Scenario ${label} row ${index} is not an array.`);
|
||||
@@ -224,14 +194,8 @@ const parseGeneralRow = (
|
||||
return {
|
||||
affinity: asNullableNumber(affinity),
|
||||
name,
|
||||
picture:
|
||||
typeof picture === 'number' || typeof picture === 'string'
|
||||
? picture
|
||||
: null,
|
||||
nation:
|
||||
typeof nation === 'number' || typeof nation === 'string'
|
||||
? nation
|
||||
: null,
|
||||
picture: typeof picture === 'number' || typeof picture === 'string' ? picture : null,
|
||||
nation: typeof nation === 'number' || typeof nation === 'string' ? nation : null,
|
||||
city: asNullableString(city),
|
||||
leadership: asNumber(leadership, 0),
|
||||
strength: asNumber(strength, 0),
|
||||
@@ -253,8 +217,7 @@ const parseGeneralRow = (
|
||||
const parseGeneralRows = (rows: unknown[], label: string): ScenarioGeneral[] =>
|
||||
rows.map((row, index) => parseGeneralRow(row, index, label));
|
||||
|
||||
const parseNationRows = (rows: unknown[]): ScenarioNation[] =>
|
||||
rows.map((row, index) => parseNationRow(row, index));
|
||||
const parseNationRows = (rows: unknown[]): ScenarioNation[] => rows.map((row, index) => parseNationRow(row, index));
|
||||
|
||||
const parseDiplomacyRows = (rows: unknown[]): ScenarioDiplomacy[] =>
|
||||
rows.map((row, index) => parseDiplomacyRow(row, index));
|
||||
@@ -267,10 +230,7 @@ export const parseScenarioDefaults = (raw: unknown): ScenarioDefaults => {
|
||||
return { stat, iconPath };
|
||||
};
|
||||
|
||||
export const parseScenarioDefinition = (
|
||||
raw: unknown,
|
||||
defaults: ScenarioDefaults
|
||||
): ScenarioDefinition => {
|
||||
export const parseScenarioDefinition = (raw: unknown, defaults: ScenarioDefaults): ScenarioDefinition => {
|
||||
// 시나리오 JSON을 런타임에서 쓰는 구조로 정규화한다.
|
||||
const data = zScenarioInput.parse(raw);
|
||||
const stat = parseScenarioStatBlock(data.stat, defaults.stat);
|
||||
@@ -285,8 +245,7 @@ export const parseScenarioDefinition = (
|
||||
};
|
||||
|
||||
const title = data.title;
|
||||
const startYear =
|
||||
typeof data.startYear === 'number' ? data.startYear : null;
|
||||
const startYear = typeof data.startYear === 'number' ? data.startYear : null;
|
||||
const life = typeof data.life === 'number' ? data.life : null;
|
||||
const fiction = typeof data.fiction === 'number' ? data.fiction : null;
|
||||
const history = asStringArray(data.history);
|
||||
@@ -295,10 +254,7 @@ export const parseScenarioDefinition = (
|
||||
const diplomacy = parseDiplomacyRows(data.diplomacy ?? []);
|
||||
const generals = parseGeneralRows(data.general ?? [], 'general');
|
||||
const generalsEx = parseGeneralRows(data.general_ex ?? [], 'general_ex');
|
||||
const generalsNeutral = parseGeneralRows(
|
||||
data.general_neutral ?? [],
|
||||
'general_neutral'
|
||||
);
|
||||
const generalsNeutral = parseGeneralRows(data.general_neutral ?? [], 'general_neutral');
|
||||
const events = data.events ?? [];
|
||||
const initialEvents = data.initialEvents ?? data.initialActions ?? [];
|
||||
|
||||
|
||||
@@ -69,9 +69,7 @@ export class GeneralActionPipeline<TriggerState extends GeneralTriggerState = Ge
|
||||
this.modules = modules.filter(Boolean) as GeneralActionModule<TriggerState>[];
|
||||
}
|
||||
|
||||
getPreTurnExecuteTriggerList(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
): GeneralTriggerCaller<TriggerState> {
|
||||
getPreTurnExecuteTriggerList(context: GeneralActionContext<TriggerState>): GeneralTriggerCaller<TriggerState> {
|
||||
const triggerCaller = new GeneralTriggerCaller<TriggerState>();
|
||||
|
||||
for (const module of this.modules) {
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import type { WorldStateRepository } from '@sammo-ts/logic/ports/world.js';
|
||||
import { TriggerCaller, type Trigger } from './core.js';
|
||||
|
||||
export interface GeneralWorldView<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface GeneralWorldView<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
listGenerals(): General<TriggerState>[];
|
||||
listGeneralsByCity?(cityId: number): General<TriggerState>[];
|
||||
}
|
||||
@@ -19,9 +17,7 @@ export interface GeneralSkillActivation {
|
||||
activate(...keys: string[]): void;
|
||||
}
|
||||
|
||||
export const createGeneralSkillActivation = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
export const createGeneralSkillActivation = <TriggerState extends GeneralTriggerState>(
|
||||
general: General<TriggerState>
|
||||
): GeneralSkillActivation => ({
|
||||
has: (key: string) => Boolean(general.triggerState.flags[key]),
|
||||
@@ -40,15 +36,14 @@ export interface GeneralActionContext<TriggerState extends GeneralTriggerState =
|
||||
rng?: RandomGenerator;
|
||||
}
|
||||
|
||||
export interface GeneralTriggerContext<TriggerState extends GeneralTriggerState = GeneralTriggerState>
|
||||
extends GeneralActionContext<TriggerState> {
|
||||
export interface GeneralTriggerContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends GeneralActionContext<TriggerState> {
|
||||
rng: RandomGenerator;
|
||||
skill: GeneralSkillActivation;
|
||||
}
|
||||
|
||||
export const createGeneralTriggerContext = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
export const createGeneralTriggerContext = <TriggerState extends GeneralTriggerState>(
|
||||
context: GeneralActionContext<TriggerState> & { rng: RandomGenerator }
|
||||
): GeneralTriggerContext<TriggerState> => ({
|
||||
...context,
|
||||
@@ -58,19 +53,19 @@ export const createGeneralTriggerContext = <
|
||||
export type GeneralTrigger<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
Env extends Record<string, unknown> = Record<string, unknown>,
|
||||
Arg = unknown
|
||||
Arg = unknown,
|
||||
> = Trigger<GeneralTriggerContext<TriggerState>, Env, Arg>;
|
||||
|
||||
export class GeneralTriggerCaller<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
Env extends Record<string, unknown> = Record<string, unknown>,
|
||||
Arg = unknown
|
||||
Arg = unknown,
|
||||
> extends TriggerCaller<GeneralTriggerContext<TriggerState>, Env, Arg> {}
|
||||
|
||||
export abstract class BaseGeneralTrigger<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
Env extends Record<string, unknown> = Record<string, unknown>,
|
||||
Arg = unknown
|
||||
Arg = unknown,
|
||||
> implements GeneralTrigger<TriggerState, Env, Arg> {
|
||||
public abstract readonly priority: number;
|
||||
|
||||
@@ -80,9 +75,5 @@ export abstract class BaseGeneralTrigger<
|
||||
return `${this.priority}_${this.constructor.name}_${this.general.id}`;
|
||||
}
|
||||
|
||||
abstract action(
|
||||
context: GeneralTriggerContext<TriggerState>,
|
||||
env: Env,
|
||||
arg?: Arg
|
||||
): Env;
|
||||
abstract action(context: GeneralTriggerContext<TriggerState>, env: Env, arg?: Arg): Env;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import { JosaUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import {
|
||||
BaseGeneralTrigger,
|
||||
type GeneralTriggerContext,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import { BaseGeneralTrigger, type GeneralTriggerContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
|
||||
const HEAL_PROBABILITY = 0.5;
|
||||
const MIN_HEAL_INJURY = 10;
|
||||
@@ -20,15 +17,13 @@ const resolveCityGenerals = <TriggerState extends GeneralTriggerState>(
|
||||
}
|
||||
const list = worldView.listGeneralsByCity
|
||||
? worldView.listGeneralsByCity(general.cityId)
|
||||
: worldView.listGenerals().filter(
|
||||
(candidate) => candidate.cityId === general.cityId
|
||||
);
|
||||
: worldView.listGenerals().filter((candidate) => candidate.cityId === general.cityId);
|
||||
return list.filter((candidate) => candidate.id !== general.id);
|
||||
};
|
||||
|
||||
// 의술 특기의 도시 치료 트리거.
|
||||
export class CheUisulCityHealTrigger<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> extends BaseGeneralTrigger<TriggerState> {
|
||||
public readonly priority = TriggerPriority.Begin + 10;
|
||||
|
||||
@@ -36,10 +31,7 @@ export class CheUisulCityHealTrigger<
|
||||
super(general);
|
||||
}
|
||||
|
||||
action(
|
||||
context: GeneralTriggerContext<TriggerState>,
|
||||
env: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
action(context: GeneralTriggerContext<TriggerState>, env: Record<string, unknown>): Record<string, unknown> {
|
||||
const general = context.general;
|
||||
const rng = context.rng;
|
||||
const logger = context.log;
|
||||
@@ -50,17 +42,15 @@ export class CheUisulCityHealTrigger<
|
||||
logger?.push('<C>의술</>을 펼쳐 스스로 치료합니다!');
|
||||
}
|
||||
|
||||
const candidates = resolveCityGenerals(general, context).filter(
|
||||
(candidate) => {
|
||||
if (candidate.injury <= MIN_HEAL_INJURY) {
|
||||
return false;
|
||||
}
|
||||
if (general.nationId === 0) {
|
||||
return candidate.nationId === 0;
|
||||
}
|
||||
return true;
|
||||
const candidates = resolveCityGenerals(general, context).filter((candidate) => {
|
||||
if (candidate.injury <= MIN_HEAL_INJURY) {
|
||||
return false;
|
||||
}
|
||||
);
|
||||
if (general.nationId === 0) {
|
||||
return candidate.nationId === 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const healed = candidates.filter(() => rng.nextBool(HEAL_PROBABILITY));
|
||||
|
||||
@@ -75,14 +65,10 @@ export class CheUisulCityHealTrigger<
|
||||
const firstName = healed[0]?.name ?? '장수';
|
||||
if (healed.length === 1) {
|
||||
const josa = JosaUtil.pick(firstName, '을');
|
||||
logger?.push(
|
||||
`<C>의술</>을 펼쳐 도시의 장수 <Y>${firstName}</>${josa} 치료합니다!`
|
||||
);
|
||||
logger?.push(`<C>의술</>을 펼쳐 도시의 장수 <Y>${firstName}</>${josa} 치료합니다!`);
|
||||
} else {
|
||||
const otherCount = healed.length - 1;
|
||||
logger?.push(
|
||||
`<C>의술</>을 펼쳐 도시의 장수들 <Y>${firstName}</> 외 <C>${otherCount}</>명을 치료합니다!`
|
||||
);
|
||||
logger?.push(`<C>의술</>을 펼쳐 도시의 장수들 <Y>${firstName}</> 외 <C>${otherCount}</>명을 치료합니다!`);
|
||||
}
|
||||
|
||||
return env;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import {
|
||||
BaseGeneralTrigger,
|
||||
type GeneralTriggerContext,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import { BaseGeneralTrigger, type GeneralTriggerContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
||||
|
||||
interface ItemHealOptions {
|
||||
@@ -24,10 +21,7 @@ export class CheItemHealTrigger extends BaseGeneralTrigger {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
action(
|
||||
context: GeneralTriggerContext,
|
||||
env: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
action(context: GeneralTriggerContext, env: Record<string, unknown>): Record<string, unknown> {
|
||||
const { general } = context;
|
||||
if (general.role.items.item !== this.options.itemKey) {
|
||||
return env;
|
||||
@@ -40,9 +34,7 @@ export class CheItemHealTrigger extends BaseGeneralTrigger {
|
||||
context.skill.activate('pre.부상경감', 'pre.치료');
|
||||
|
||||
const josa = JosaUtil.pick(this.options.itemRawName, '을');
|
||||
context.log?.push(
|
||||
`<C>${this.options.itemName}</>${josa} 사용하여 치료합니다!`
|
||||
);
|
||||
context.log?.push(`<C>${this.options.itemName}</>${josa} 사용하여 치료합니다!`);
|
||||
|
||||
if (this.options.consume()) {
|
||||
general.role.items.item = null;
|
||||
|
||||
@@ -1,45 +1,25 @@
|
||||
import type {
|
||||
TraitModule,
|
||||
TraitModuleExport,
|
||||
} from '@sammo-ts/logic/triggers/special/types.js';
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
|
||||
|
||||
export const DOMESTIC_TRAIT_KEYS = [
|
||||
'che_인덕',
|
||||
'che_발명',
|
||||
] as const;
|
||||
export const DOMESTIC_TRAIT_KEYS = ['che_인덕', 'che_발명'] as const;
|
||||
|
||||
export type DomesticTraitKey =
|
||||
(typeof DOMESTIC_TRAIT_KEYS)[number];
|
||||
export type DomesticTraitKey = (typeof DOMESTIC_TRAIT_KEYS)[number];
|
||||
|
||||
export type DomesticTraitModule = TraitModule;
|
||||
|
||||
export type DomesticTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<
|
||||
DomesticTraitKey,
|
||||
DomesticTraitImporter
|
||||
> = {
|
||||
const defaultImporters: Record<DomesticTraitKey, DomesticTraitImporter> = {
|
||||
che_인덕: async () => import('./che_인덕.js'),
|
||||
che_발명: async () => import('./che_발명.js'),
|
||||
};
|
||||
|
||||
export const isDomesticTraitKey = (
|
||||
value: string
|
||||
): value is DomesticTraitKey =>
|
||||
export const isDomesticTraitKey = (value: string): value is DomesticTraitKey =>
|
||||
DOMESTIC_TRAIT_KEYS.includes(value as DomesticTraitKey);
|
||||
|
||||
export class DomesticTraitLoader {
|
||||
private readonly cache = new Map<
|
||||
DomesticTraitKey,
|
||||
Promise<DomesticTraitModule>
|
||||
>();
|
||||
private readonly cache = new Map<DomesticTraitKey, Promise<DomesticTraitModule>>();
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<
|
||||
DomesticTraitKey,
|
||||
DomesticTraitImporter
|
||||
> = defaultImporters
|
||||
) {}
|
||||
constructor(private readonly importers: Record<DomesticTraitKey, DomesticTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: DomesticTraitKey): Promise<DomesticTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
@@ -56,14 +36,10 @@ export class DomesticTraitLoader {
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(
|
||||
`Domestic trait key mismatch: expected ${key}, got ${resolved.key}`
|
||||
);
|
||||
throw new Error(`Domestic trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'domestic') {
|
||||
throw new Error(
|
||||
`Domestic trait kind mismatch: ${resolved.key}`
|
||||
);
|
||||
throw new Error(`Domestic trait kind mismatch: ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
|
||||
@@ -39,5 +39,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -39,5 +39,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -40,5 +40,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -51,5 +51,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -40,5 +40,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -40,5 +40,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -39,5 +39,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -40,5 +40,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -40,5 +40,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -39,5 +39,5 @@ export const traitModule: TraitModule = {
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule["onCalcStat"], undefined>,
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
TraitModule,
|
||||
TraitModuleExport,
|
||||
} from '@sammo-ts/logic/triggers/special/types.js';
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
|
||||
|
||||
export const PERSONALITY_TRAIT_KEYS = [
|
||||
'che_안전',
|
||||
@@ -17,17 +14,13 @@ export const PERSONALITY_TRAIT_KEYS = [
|
||||
'che_은둔',
|
||||
] as const;
|
||||
|
||||
export type PersonalityTraitKey =
|
||||
(typeof PERSONALITY_TRAIT_KEYS)[number];
|
||||
export type PersonalityTraitKey = (typeof PERSONALITY_TRAIT_KEYS)[number];
|
||||
|
||||
export type PersonalityTraitModule = TraitModule;
|
||||
|
||||
export type PersonalityTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<
|
||||
PersonalityTraitKey,
|
||||
PersonalityTraitImporter
|
||||
> = {
|
||||
const defaultImporters: Record<PersonalityTraitKey, PersonalityTraitImporter> = {
|
||||
che_안전: async () => import('./che_안전.js'),
|
||||
che_유지: async () => import('./che_유지.js'),
|
||||
che_재간: async () => import('./che_재간.js'),
|
||||
@@ -41,23 +34,13 @@ const defaultImporters: Record<
|
||||
che_은둔: async () => import('./che_은둔.js'),
|
||||
};
|
||||
|
||||
export const isPersonalityTraitKey = (
|
||||
value: string
|
||||
): value is PersonalityTraitKey =>
|
||||
export const isPersonalityTraitKey = (value: string): value is PersonalityTraitKey =>
|
||||
PERSONALITY_TRAIT_KEYS.includes(value as PersonalityTraitKey);
|
||||
|
||||
export class PersonalityTraitLoader {
|
||||
private readonly cache = new Map<
|
||||
PersonalityTraitKey,
|
||||
Promise<PersonalityTraitModule>
|
||||
>();
|
||||
private readonly cache = new Map<PersonalityTraitKey, Promise<PersonalityTraitModule>>();
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<
|
||||
PersonalityTraitKey,
|
||||
PersonalityTraitImporter
|
||||
> = defaultImporters
|
||||
) {}
|
||||
constructor(private readonly importers: Record<PersonalityTraitKey, PersonalityTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: PersonalityTraitKey): Promise<PersonalityTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
@@ -74,14 +57,10 @@ export class PersonalityTraitLoader {
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(
|
||||
`Personality trait key mismatch: expected ${key}, got ${resolved.key}`
|
||||
);
|
||||
throw new Error(`Personality trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'personality') {
|
||||
throw new Error(
|
||||
`Personality trait kind mismatch: ${resolved.key}`
|
||||
);
|
||||
throw new Error(`Personality trait kind mismatch: ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
|
||||
@@ -15,11 +15,7 @@ import type {
|
||||
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import type {
|
||||
TraitKind,
|
||||
TraitModule,
|
||||
TraitModuleRegistry,
|
||||
} from './types.js';
|
||||
import type { TraitKind, TraitModule, TraitModuleRegistry } from './types.js';
|
||||
|
||||
const resolveTraitKey = (
|
||||
context: {
|
||||
@@ -63,23 +59,19 @@ const resolveModule = <TriggerState extends GeneralTriggerState>(
|
||||
|
||||
// General 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
|
||||
export class TraitGeneralActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionModule<TriggerState> {
|
||||
constructor(
|
||||
private readonly kind: TraitKind,
|
||||
private readonly registry: TraitModuleRegistry<TriggerState>
|
||||
) {}
|
||||
|
||||
private getModule(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
): TraitModule<TriggerState> | null {
|
||||
private getModule(context: GeneralActionContext<TriggerState>): TraitModule<TriggerState> | null {
|
||||
const key = resolveTraitKey(context, this.kind);
|
||||
return resolveModule(this.registry, this.kind, key);
|
||||
}
|
||||
|
||||
getPreTurnExecuteTriggerList(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
) {
|
||||
getPreTurnExecuteTriggerList(context: GeneralActionContext<TriggerState>) {
|
||||
const module = this.getModule(context);
|
||||
return module?.getPreTurnExecuteTriggerList?.(context) ?? null;
|
||||
}
|
||||
@@ -112,9 +104,7 @@ export class TraitGeneralActionRouter<
|
||||
aux?: unknown
|
||||
): number {
|
||||
const module = this.getModule(context);
|
||||
return (
|
||||
module?.onCalcOpposeStat?.(context, statName, value, aux) ?? value
|
||||
);
|
||||
return module?.onCalcOpposeStat?.(context, statName, value, aux) ?? value;
|
||||
}
|
||||
|
||||
onCalcStrategic(
|
||||
@@ -143,42 +133,31 @@ export class TraitGeneralActionRouter<
|
||||
aux?: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
const module = this.getModule(context);
|
||||
const result = module?.onArbitraryAction?.(
|
||||
context,
|
||||
actionType,
|
||||
phase,
|
||||
aux
|
||||
);
|
||||
return result === undefined ? aux ?? null : result;
|
||||
const result = module?.onArbitraryAction?.(context, actionType, phase, aux);
|
||||
return result === undefined ? (aux ?? null) : result;
|
||||
}
|
||||
}
|
||||
|
||||
// 전투 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
|
||||
export class TraitWarActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements WarActionModule<TriggerState> {
|
||||
constructor(
|
||||
private readonly kind: TraitKind,
|
||||
private readonly registry: TraitModuleRegistry<TriggerState>
|
||||
) {}
|
||||
|
||||
private getModule(
|
||||
context: WarActionContext<TriggerState>
|
||||
): TraitModule<TriggerState> | null {
|
||||
private getModule(context: WarActionContext<TriggerState>): TraitModule<TriggerState> | null {
|
||||
const key = resolveTraitKey(context, this.kind);
|
||||
return resolveModule(this.registry, this.kind, key);
|
||||
}
|
||||
|
||||
getBattleInitTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null {
|
||||
getBattleInitTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
|
||||
const module = this.getModule(context);
|
||||
return module?.getBattleInitTriggerList?.(context) ?? null;
|
||||
}
|
||||
|
||||
getBattlePhaseTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null {
|
||||
getBattlePhaseTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
|
||||
const module = this.getModule(context);
|
||||
return module?.getBattlePhaseTriggerList?.(context) ?? null;
|
||||
}
|
||||
@@ -213,16 +192,12 @@ export class TraitWarActionRouter<
|
||||
}
|
||||
}
|
||||
|
||||
export interface TraitModuleSet<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface TraitModuleSet<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
general: GeneralActionModule<TriggerState>[];
|
||||
war: WarActionModule<TriggerState>[];
|
||||
}
|
||||
|
||||
export const createTraitModuleRegistry = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(options: {
|
||||
export const createTraitModuleRegistry = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(options: {
|
||||
domestic?: TraitModule<TriggerState>[];
|
||||
war?: TraitModule<TriggerState>[];
|
||||
personality?: TraitModule<TriggerState>[];
|
||||
@@ -245,9 +220,7 @@ export const createTraitModuleRegistry = <
|
||||
};
|
||||
|
||||
// 특성 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다.
|
||||
export const createTraitModules = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const createTraitModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
registry: TraitModuleRegistry<TriggerState>
|
||||
): TraitModuleSet<TriggerState> => ({
|
||||
general: [
|
||||
|
||||
@@ -11,21 +11,15 @@ export interface TraitSpec {
|
||||
kind: TraitKind;
|
||||
}
|
||||
|
||||
export type TraitModule<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> = TraitSpec &
|
||||
export type TraitModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> = TraitSpec &
|
||||
GeneralActionModule<TriggerState> &
|
||||
WarActionModule<TriggerState>;
|
||||
|
||||
export interface TraitModuleExport<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface TraitModuleExport<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
traitModule: TraitModule<TriggerState>;
|
||||
}
|
||||
|
||||
export interface TraitModuleRegistry<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export interface TraitModuleRegistry<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
domestic: Map<string, TraitModule<TriggerState>>;
|
||||
war: Map<string, TraitModule<TriggerState>>;
|
||||
personality: Map<string, TraitModule<TriggerState>>;
|
||||
|
||||
@@ -13,8 +13,7 @@ export const traitModule: TraitModule = {
|
||||
getName: () => '의술',
|
||||
getInfo: () =>
|
||||
'[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)',
|
||||
getPreTurnExecuteTriggerList: (context) =>
|
||||
new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general)),
|
||||
getPreTurnExecuteTriggerList: (context) => new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general)),
|
||||
getBattlePhaseTriggerList: (context: WarActionContext) => {
|
||||
const unit = context.unit;
|
||||
if (!unit) {
|
||||
|
||||
@@ -17,12 +17,7 @@ const resolveLeadershipBonus = (
|
||||
return value + base * 0.25;
|
||||
};
|
||||
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
|
||||
@@ -1,45 +1,24 @@
|
||||
import type {
|
||||
TraitModule,
|
||||
TraitModuleExport,
|
||||
} from '@sammo-ts/logic/triggers/special/types.js';
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
|
||||
|
||||
export const WAR_TRAIT_KEYS = [
|
||||
'che_의술',
|
||||
'che_징병',
|
||||
] as const;
|
||||
export const WAR_TRAIT_KEYS = ['che_의술', 'che_징병'] as const;
|
||||
|
||||
export type WarTraitKey =
|
||||
(typeof WAR_TRAIT_KEYS)[number];
|
||||
export type WarTraitKey = (typeof WAR_TRAIT_KEYS)[number];
|
||||
|
||||
export type WarTraitModule = TraitModule;
|
||||
|
||||
export type WarTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<
|
||||
WarTraitKey,
|
||||
WarTraitImporter
|
||||
> = {
|
||||
const defaultImporters: Record<WarTraitKey, WarTraitImporter> = {
|
||||
che_의술: async () => import('./che_의술.js'),
|
||||
che_징병: async () => import('./che_징병.js'),
|
||||
};
|
||||
|
||||
export const isWarTraitKey = (
|
||||
value: string
|
||||
): value is WarTraitKey =>
|
||||
WAR_TRAIT_KEYS.includes(value as WarTraitKey);
|
||||
export const isWarTraitKey = (value: string): value is WarTraitKey => WAR_TRAIT_KEYS.includes(value as WarTraitKey);
|
||||
|
||||
export class WarTraitLoader {
|
||||
private readonly cache = new Map<
|
||||
WarTraitKey,
|
||||
Promise<WarTraitModule>
|
||||
>();
|
||||
private readonly cache = new Map<WarTraitKey, Promise<WarTraitModule>>();
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<
|
||||
WarTraitKey,
|
||||
WarTraitImporter
|
||||
> = defaultImporters
|
||||
) {}
|
||||
constructor(private readonly importers: Record<WarTraitKey, WarTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: WarTraitKey): Promise<WarTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
@@ -56,9 +35,7 @@ export class WarTraitLoader {
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(
|
||||
`War trait key mismatch: expected ${key}, got ${resolved.key}`
|
||||
);
|
||||
throw new Error(`War trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'war') {
|
||||
throw new Error(`War trait kind mismatch: ${resolved.key}`);
|
||||
|
||||
@@ -14,35 +14,15 @@ export type TriggerDomesticActionType =
|
||||
| '모병'
|
||||
| '단련';
|
||||
|
||||
export type TriggerDomesticVarType =
|
||||
| 'cost'
|
||||
| 'score'
|
||||
| 'success'
|
||||
| 'fail'
|
||||
| 'train'
|
||||
| 'atmos'
|
||||
| 'rice'
|
||||
| 'probability';
|
||||
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';
|
||||
|
||||
export type TriggerStrategicActionType =
|
||||
| '의병모집'
|
||||
| '허보'
|
||||
| '필사즉생'
|
||||
| '백성동원'
|
||||
| '이호경식'
|
||||
| '수몰'
|
||||
| '급습';
|
||||
export type TriggerStrategicActionType = '의병모집' | '허보' | '필사즉생' | '백성동원' | '이호경식' | '수몰' | '급습';
|
||||
|
||||
export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
||||
|
||||
export type TriggerNationalIncomeType = 'gold' | 'rice';
|
||||
|
||||
export type GeneralStatName =
|
||||
| 'leadership'
|
||||
| 'strength'
|
||||
| 'intelligence'
|
||||
| 'experience'
|
||||
| 'dedication';
|
||||
export type GeneralStatName = 'leadership' | 'strength' | 'intelligence' | 'experience' | 'dedication';
|
||||
|
||||
export type WarStatName =
|
||||
| GeneralStatName
|
||||
|
||||
@@ -11,21 +11,12 @@ export interface TurnSchedule {
|
||||
|
||||
const MINUTES_PER_DAY = 24 * 60;
|
||||
|
||||
const toMinuteOfDay = (date: Date): number =>
|
||||
date.getHours() * 60 + date.getMinutes();
|
||||
const toMinuteOfDay = (date: Date): number => date.getHours() * 60 + date.getMinutes();
|
||||
|
||||
const toLocalDateAtMinute = (date: Date, minuteOfDay: number, dayOffset = 0): Date => {
|
||||
const hour = Math.floor(minuteOfDay / 60);
|
||||
const minute = minuteOfDay % 60;
|
||||
return new Date(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate() + dayOffset,
|
||||
hour,
|
||||
minute,
|
||||
0,
|
||||
0
|
||||
);
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + dayOffset, hour, minute, 0, 0);
|
||||
};
|
||||
|
||||
const normalizeEntries = (entries: TurnScheduleEntries): TurnScheduleEntries => {
|
||||
@@ -39,10 +30,7 @@ const normalizeEntries = (entries: TurnScheduleEntries): TurnScheduleEntries =>
|
||||
return normalized as TurnScheduleEntries;
|
||||
};
|
||||
|
||||
const findCurrentEntryIndex = (
|
||||
minuteOfDay: number,
|
||||
entries: TurnScheduleEntries
|
||||
): number => {
|
||||
const findCurrentEntryIndex = (minuteOfDay: number, entries: TurnScheduleEntries): number => {
|
||||
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
||||
const entry = entries[i];
|
||||
if (entry && entry.startMinute <= minuteOfDay) {
|
||||
@@ -52,10 +40,7 @@ const findCurrentEntryIndex = (
|
||||
return -1;
|
||||
};
|
||||
|
||||
const getEntryAt = (
|
||||
entries: TurnScheduleEntries,
|
||||
index: number
|
||||
): TurnScheduleEntry =>
|
||||
const getEntryAt = (entries: TurnScheduleEntries, index: number): TurnScheduleEntry =>
|
||||
entries[Math.max(0, Math.min(entries.length - 1, index))] ?? entries[0];
|
||||
|
||||
export const getTickMinutesAt = (date: Date, schedule: TurnSchedule): number => {
|
||||
@@ -77,22 +62,12 @@ export const getNextTurnAt = (date: Date, schedule: TurnSchedule): Date => {
|
||||
|
||||
const currentEntry = getEntryAt(entries, currentIndex);
|
||||
const nextEntry = getEntryAt(entries, nextIndex);
|
||||
const segmentStart = toLocalDateAtMinute(
|
||||
date,
|
||||
currentEntry.startMinute,
|
||||
startDayOffset
|
||||
);
|
||||
const segmentEnd = toLocalDateAtMinute(
|
||||
date,
|
||||
nextEntry.startMinute,
|
||||
nextDayOffset
|
||||
);
|
||||
const segmentStart = toLocalDateAtMinute(date, currentEntry.startMinute, startDayOffset);
|
||||
const segmentEnd = toLocalDateAtMinute(date, nextEntry.startMinute, nextDayOffset);
|
||||
|
||||
const elapsedMinutes = (date.getTime() - segmentStart.getTime()) / 60000;
|
||||
const nextStep = Math.floor(elapsedMinutes / currentEntry.tickMinutes) + 1;
|
||||
const nextCandidate = new Date(
|
||||
segmentStart.getTime() + nextStep * currentEntry.tickMinutes * 60000
|
||||
);
|
||||
const nextCandidate = new Date(segmentStart.getTime() + nextStep * currentEntry.tickMinutes * 60000);
|
||||
|
||||
if (nextCandidate.getTime() < segmentEnd.getTime()) {
|
||||
return nextCandidate;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { RandUtil } from '@sammo-ts/common';
|
||||
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
||||
import type { WarStatName } from '@sammo-ts/logic/triggers/types.js';
|
||||
import type { WarUnit } from './units.js';
|
||||
@@ -24,13 +19,9 @@ export interface WarActionModule<TriggerState extends GeneralTriggerState = Gene
|
||||
getName?(): string;
|
||||
getInfo?(): string;
|
||||
|
||||
getBattleInitTriggerList?(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null;
|
||||
getBattleInitTriggerList?(context: WarActionContext<TriggerState>): WarTriggerCaller | null;
|
||||
|
||||
getBattlePhaseTriggerList?(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null;
|
||||
getBattlePhaseTriggerList?(context: WarActionContext<TriggerState>): WarTriggerCaller | null;
|
||||
|
||||
onCalcStat?(
|
||||
context: WarActionContext<TriggerState>,
|
||||
@@ -53,9 +44,7 @@ export interface WarActionModule<TriggerState extends GeneralTriggerState = Gene
|
||||
): [number, number];
|
||||
}
|
||||
|
||||
export class WarActionPipeline<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
export class WarActionPipeline<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
// 전투용 iAction 파이프라인: 스탯/트리거/전투력 보정 흐름을 순서대로 적용한다.
|
||||
private readonly modules: WarActionModule<TriggerState>[];
|
||||
|
||||
@@ -63,9 +52,7 @@ export class WarActionPipeline<
|
||||
this.modules = modules.filter(Boolean) as WarActionModule<TriggerState>[];
|
||||
}
|
||||
|
||||
getBattleInitTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller {
|
||||
getBattleInitTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller {
|
||||
const caller = new WarTriggerCaller();
|
||||
for (const module of this.modules) {
|
||||
const triggers = module.getBattleInitTriggerList?.(context);
|
||||
@@ -76,9 +63,7 @@ export class WarActionPipeline<
|
||||
return caller;
|
||||
}
|
||||
|
||||
getBattlePhaseTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller {
|
||||
getBattlePhaseTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller {
|
||||
const caller = new WarTriggerCaller();
|
||||
for (const module of this.modules) {
|
||||
const triggers = module.getBattlePhaseTriggerList?.(context);
|
||||
@@ -132,11 +117,7 @@ export class WarActionPipeline<
|
||||
if (!module.getWarPowerMultiplier) {
|
||||
continue;
|
||||
}
|
||||
const [attMul, defMul] = module.getWarPowerMultiplier(
|
||||
context,
|
||||
unit,
|
||||
oppose
|
||||
);
|
||||
const [attMul, defMul] = module.getWarPowerMultiplier(context, unit, oppose);
|
||||
attack *= attMul;
|
||||
defence *= defMul;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import {
|
||||
JosaUtil,
|
||||
LiteHashDRBG,
|
||||
RandUtil,
|
||||
} from '@sammo-ts/common';
|
||||
import { JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
||||
import { LogFormat, type LogEntryDraft } from '@sammo-ts/logic/logging/types.js';
|
||||
import {
|
||||
buildCrewTypeIndex,
|
||||
getTechCost,
|
||||
getTechLevel,
|
||||
} from '@sammo-ts/logic/world/unitSet.js';
|
||||
import { buildCrewTypeIndex, getTechCost, getTechLevel } from '@sammo-ts/logic/world/unitSet.js';
|
||||
import type { WarUnitReport } from './types.js';
|
||||
import type {
|
||||
ConquerCityOutcome,
|
||||
@@ -26,21 +13,12 @@ import type {
|
||||
WarAftermathTechContext,
|
||||
WarDiplomacyDelta,
|
||||
} from './types.js';
|
||||
import {
|
||||
clamp,
|
||||
clampMin,
|
||||
getMetaNumber,
|
||||
round,
|
||||
simpleSerialize,
|
||||
} from './utils.js';
|
||||
import { clamp, clampMin, getMetaNumber, round, simpleSerialize } from './utils.js';
|
||||
|
||||
const META_DEAD = 'dead';
|
||||
const META_CONFLICT = 'conflict';
|
||||
|
||||
const findReport = (
|
||||
reports: WarUnitReport[],
|
||||
predicate: (report: WarUnitReport) => boolean
|
||||
): WarUnitReport | null => {
|
||||
const findReport = (reports: WarUnitReport[], predicate: (report: WarUnitReport) => boolean): WarUnitReport | null => {
|
||||
for (const report of reports) {
|
||||
if (predicate(report)) {
|
||||
return report;
|
||||
@@ -49,8 +27,7 @@ const findReport = (
|
||||
return null;
|
||||
};
|
||||
|
||||
const getDeadCounter = (city: City): number =>
|
||||
getMetaNumber(city.meta, META_DEAD, 0);
|
||||
const getDeadCounter = (city: City): number => getMetaNumber(city.meta, META_DEAD, 0);
|
||||
|
||||
const setDeadCounter = (city: City, value: number): void => {
|
||||
city.meta[META_DEAD] = round(value);
|
||||
@@ -67,19 +44,12 @@ const isSupplyCity = (city: City): boolean => {
|
||||
return city.supplyState > 0;
|
||||
};
|
||||
|
||||
const resolveCityTrainAtmos = (year: number, startYear: number): number =>
|
||||
clamp(year - startYear + 59, 60, 110);
|
||||
const resolveCityTrainAtmos = (year: number, startYear: number): number => clamp(year - startYear + 59, 60, 110);
|
||||
|
||||
const isTechLimited = (
|
||||
tech: number,
|
||||
year: number,
|
||||
startYear: number,
|
||||
config: WarAftermathConfig
|
||||
): boolean => {
|
||||
const isTechLimited = (tech: number, year: number, startYear: number, config: WarAftermathConfig): boolean => {
|
||||
const relYear = clampMin(year - startYear, 0);
|
||||
const relMaxTech = clamp(
|
||||
Math.floor(relYear / config.techLevelIncYear) +
|
||||
config.initialAllowedTechLevel,
|
||||
Math.floor(relYear / config.techLevelIncYear) + config.initialAllowedTechLevel,
|
||||
1,
|
||||
config.maxTechLevel
|
||||
);
|
||||
@@ -92,15 +62,9 @@ const resolveNationGenCount = <TriggerState extends GeneralTriggerState>(
|
||||
generals: General<TriggerState>[],
|
||||
config: WarAftermathConfig
|
||||
): { total: number; effective: number } => {
|
||||
const fallback = generals.filter(
|
||||
(general) => general.nationId === nation.id
|
||||
).length;
|
||||
const fallback = generals.filter((general) => general.nationId === nation.id).length;
|
||||
let total = getMetaNumber(nation.meta, 'gennum', fallback);
|
||||
let effective = generals.filter(
|
||||
(general) =>
|
||||
general.nationId === nation.id &&
|
||||
general.npcState !== 5
|
||||
).length;
|
||||
let effective = generals.filter((general) => general.nationId === nation.id && general.npcState !== 5).length;
|
||||
|
||||
if (effective < config.initialNationGenLimit) {
|
||||
total = config.initialNationGenLimit;
|
||||
@@ -126,11 +90,7 @@ const applyNationTechGain = <TriggerState extends GeneralTriggerState>(
|
||||
});
|
||||
}
|
||||
|
||||
const { total, effective } = resolveNationGenCount(
|
||||
nation,
|
||||
input.generals,
|
||||
config
|
||||
);
|
||||
const { total, effective } = resolveNationGenCount(nation, input.generals, config);
|
||||
|
||||
if (total !== effective) {
|
||||
gain *= total / effective;
|
||||
@@ -145,10 +105,7 @@ const applyNationTechGain = <TriggerState extends GeneralTriggerState>(
|
||||
nation.meta.tech = round(tech);
|
||||
};
|
||||
|
||||
const resolveConquerNation = (
|
||||
city: City,
|
||||
attackerNationId: number
|
||||
): number => {
|
||||
const resolveConquerNation = (city: City, attackerNationId: number): number => {
|
||||
const rawConflict = city.meta[META_CONFLICT];
|
||||
if (!rawConflict) {
|
||||
return attackerNationId;
|
||||
@@ -183,26 +140,20 @@ const findNextCapital = (
|
||||
capturedCityId: number,
|
||||
oldCapital: City
|
||||
): City | null => {
|
||||
const candidates = cities.filter(
|
||||
(city) => city.nationId === defenderNationId && city.id !== capturedCityId
|
||||
);
|
||||
const candidates = cities.filter((city) => city.nationId === defenderNationId && city.id !== capturedCityId);
|
||||
if (!candidates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const oldPos = getCityPosition(oldCapital);
|
||||
if (!oldPos) {
|
||||
return candidates.sort(
|
||||
(lhs, rhs) => rhs.population - lhs.population
|
||||
)[0]!;
|
||||
return candidates.sort((lhs, rhs) => rhs.population - lhs.population)[0]!;
|
||||
}
|
||||
|
||||
return candidates
|
||||
.map((city) => {
|
||||
const pos = getCityPosition(city);
|
||||
const distance = pos
|
||||
? Math.hypot(pos.x - oldPos.x, pos.y - oldPos.y)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const distance = pos ? Math.hypot(pos.x - oldPos.x, pos.y - oldPos.y) : Number.MAX_SAFE_INTEGER;
|
||||
return { city, distance };
|
||||
})
|
||||
.sort((lhs, rhs) => {
|
||||
@@ -224,14 +175,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
input: WarAftermathInput<TriggerState>,
|
||||
rng: RandUtil
|
||||
): ConquerCityOutcome<TriggerState> => {
|
||||
const {
|
||||
attackerNation,
|
||||
defenderNation,
|
||||
defenderCity,
|
||||
cities,
|
||||
generals,
|
||||
config,
|
||||
} = input;
|
||||
const { attackerNation, defenderNation, defenderCity, cities, generals, config } = input;
|
||||
const attacker = input.battle.attacker;
|
||||
|
||||
const logs: LogEntryDraft[] = [];
|
||||
@@ -247,9 +191,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
|
||||
const defenderNationId = defenderNation?.id ?? 0;
|
||||
const defenderNationName = defenderNation?.name ?? '공백지';
|
||||
const defenderNationDecoration = defenderNationId
|
||||
? `<D><b>${defenderNationName}</b></>의`
|
||||
: '공백지인';
|
||||
const defenderNationDecoration = defenderNationId ? `<D><b>${defenderNationName}</b></>의` : '공백지인';
|
||||
|
||||
const attackerNationName = attackerNation.name;
|
||||
const attackerGeneralName = attacker.name;
|
||||
@@ -260,13 +202,8 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
const josaYiGen = JosaUtil.pick(attackerGeneralName, '이');
|
||||
const josaYiCity = JosaUtil.pick(cityName, '이');
|
||||
|
||||
attackerLogger.pushGeneralActionLog(
|
||||
`<G><b>${cityName}</b></> 공략에 <S>성공</>했습니다.`,
|
||||
LogFormat.PLAIN
|
||||
);
|
||||
attackerLogger.pushGeneralHistoryLog(
|
||||
`<G><b>${cityName}</b></>${josaUl} <S>점령</>`
|
||||
);
|
||||
attackerLogger.pushGeneralActionLog(`<G><b>${cityName}</b></> 공략에 <S>성공</>했습니다.`, LogFormat.PLAIN);
|
||||
attackerLogger.pushGeneralHistoryLog(`<G><b>${cityName}</b></>${josaUl} <S>점령</>`);
|
||||
attackerLogger.pushGlobalActionLog(
|
||||
`<Y>${attackerGeneralName}</>${josaYiGen} <G><b>${cityName}</b></> 공략에 <S>성공</>했습니다.`
|
||||
);
|
||||
@@ -285,9 +222,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
pushLoggers([defenderNationLogger], logs);
|
||||
}
|
||||
|
||||
const defenderCityCount = defenderNationId
|
||||
? cities.filter((city) => city.nationId === defenderNationId).length
|
||||
: 0;
|
||||
const defenderCityCount = defenderNationId ? cities.filter((city) => city.nationId === defenderNationId).length : 0;
|
||||
const nationCollapsed = defenderNationId !== 0 && defenderCityCount === 1;
|
||||
|
||||
let collapseRewardGold = 0;
|
||||
@@ -295,9 +230,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
|
||||
// 국가 붕괴 시 자원 손실과 포상 정산.
|
||||
if (nationCollapsed && defenderNation) {
|
||||
const defenderGenerals = generals.filter(
|
||||
(general) => general.nationId === defenderNationId
|
||||
);
|
||||
const defenderGenerals = generals.filter((general) => general.nationId === defenderNationId);
|
||||
let totalGoldLoss = 0;
|
||||
let totalRiceLoss = 0;
|
||||
|
||||
@@ -324,12 +257,8 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
affectedGenerals.add(general);
|
||||
}
|
||||
|
||||
collapseRewardGold =
|
||||
Math.max(0, defenderNation.gold - config.baseGold) * 0.5 +
|
||||
totalGoldLoss * 0.5;
|
||||
collapseRewardRice =
|
||||
Math.max(0, defenderNation.rice - config.baseRice) * 0.5 +
|
||||
totalRiceLoss * 0.5;
|
||||
collapseRewardGold = Math.max(0, defenderNation.gold - config.baseGold) * 0.5 + totalGoldLoss * 0.5;
|
||||
collapseRewardRice = Math.max(0, defenderNation.rice - config.baseRice) * 0.5 + totalRiceLoss * 0.5;
|
||||
|
||||
attackerNation.gold = round(attackerNation.gold + collapseRewardGold);
|
||||
attackerNation.rice = round(attackerNation.rice + collapseRewardRice);
|
||||
@@ -341,12 +270,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
|
||||
// 수도 함락 시 수도 이전 및 내부 사기/자원 페널티.
|
||||
if (!nationCollapsed && defenderNation && defenderNation.capitalCityId === defenderCity.id) {
|
||||
const nextCapital = findNextCapital(
|
||||
cities,
|
||||
defenderNationId,
|
||||
defenderCity.id,
|
||||
defenderCity
|
||||
);
|
||||
const nextCapital = findNextCapital(cities, defenderNationId, defenderCity.id, defenderCity);
|
||||
if (nextCapital) {
|
||||
defenderNation.capitalCityId = nextCapital.id;
|
||||
defenderNation.gold = round(defenderNation.gold * 0.5);
|
||||
@@ -373,8 +297,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
const conquerNation =
|
||||
conquerNationId === attackerNation.id
|
||||
? attackerNation
|
||||
: input.nations.find((nation) => nation.id === conquerNationId) ??
|
||||
attackerNation;
|
||||
: (input.nations.find((nation) => nation.id === conquerNationId) ?? attackerNation);
|
||||
|
||||
if (conquerNationId === attackerNation.id) {
|
||||
attacker.cityId = defenderCity.id;
|
||||
@@ -431,9 +354,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveWarAftermath = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
export const resolveWarAftermath = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
input: WarAftermathInput<TriggerState>
|
||||
): WarAftermathOutcome<TriggerState> => {
|
||||
const logs: LogEntryDraft[] = [];
|
||||
@@ -442,14 +363,8 @@ export const resolveWarAftermath = <
|
||||
const affectedCities = new Set<City>();
|
||||
const affectedGenerals = new Set<General<TriggerState>>();
|
||||
|
||||
const attackerReport = findReport(
|
||||
input.battle.reports,
|
||||
(report) => report.type === 'general' && report.isAttacker
|
||||
);
|
||||
const cityReport = findReport(
|
||||
input.battle.reports,
|
||||
(report) => report.type === 'city'
|
||||
);
|
||||
const attackerReport = findReport(input.battle.reports, (report) => report.type === 'general' && report.isAttacker);
|
||||
const cityReport = findReport(input.battle.reports, (report) => report.type === 'city');
|
||||
|
||||
const attackerKilled = attackerReport?.killed ?? 0;
|
||||
const attackerDead = attackerReport?.dead ?? 0;
|
||||
@@ -484,10 +399,7 @@ export const resolveWarAftermath = <
|
||||
defenderNation.rice = clampMin(defenderNation.rice - rice, 0);
|
||||
affectedNations.add(defenderNation);
|
||||
} else if (input.battle.conquered) {
|
||||
const bonus =
|
||||
defenderNation.capitalCityId === input.defenderCity.id
|
||||
? 1000
|
||||
: 500;
|
||||
const bonus = defenderNation.capitalCityId === input.defenderCity.id ? 1000 : 500;
|
||||
defenderNation.rice = round(defenderNation.rice + bonus);
|
||||
affectedNations.add(defenderNation);
|
||||
}
|
||||
@@ -496,31 +408,21 @@ export const resolveWarAftermath = <
|
||||
// 기술 경험치와 외교 사망자 수치 갱신.
|
||||
if (input.attackerNation.id && attackerReport) {
|
||||
const attackerTechGain = attackerDead * 0.012;
|
||||
applyNationTechGain(
|
||||
input.attackerNation,
|
||||
attackerTechGain,
|
||||
input,
|
||||
{
|
||||
side: 'attacker',
|
||||
nation: input.attackerNation,
|
||||
attackerReport,
|
||||
}
|
||||
);
|
||||
applyNationTechGain(input.attackerNation, attackerTechGain, input, {
|
||||
side: 'attacker',
|
||||
nation: input.attackerNation,
|
||||
attackerReport,
|
||||
});
|
||||
affectedNations.add(input.attackerNation);
|
||||
}
|
||||
|
||||
if (input.defenderNation && input.defenderNation.id !== 0 && attackerReport) {
|
||||
const defenderTechGain = attackerKilled * 0.009;
|
||||
applyNationTechGain(
|
||||
input.defenderNation,
|
||||
defenderTechGain,
|
||||
input,
|
||||
{
|
||||
side: 'defender',
|
||||
nation: input.defenderNation,
|
||||
attackerReport,
|
||||
}
|
||||
);
|
||||
applyNationTechGain(input.defenderNation, defenderTechGain, input, {
|
||||
side: 'defender',
|
||||
nation: input.defenderNation,
|
||||
attackerReport,
|
||||
});
|
||||
affectedNations.add(input.defenderNation);
|
||||
|
||||
diplomacyDeltas.push(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user