Files
core2026/packages/logic/src/actions/engine.ts
T
Hide_D ee719df8fe feat: add 화계 (Fire Attack) action for generals
- Implemented the 화계 action in the general turn actions, allowing generals to perform sabotage on enemy cities.
- Created CommandResolver and ActionResolver classes to handle the logic and resolution of the action.
- Added necessary constraints to ensure valid conditions for executing the action, including checks for city occupation and resource requirements.
- Updated the general action index to include the new 화계 action.
- Introduced evaluation functions for constraints to manage action prerequisites effectively.
- Enhanced logging for successful and failed attempts of the 화계 action, providing detailed feedback on outcomes.
2025-12-29 09:38:32 +00:00

377 lines
11 KiB
TypeScript

import type { RandomGenerator } from '@sammo-ts/common';
import type {
City,
General,
GeneralRole,
GeneralTriggerState,
CityId,
GeneralId,
Nation,
NationId,
StatBlock,
} from '../domain/entities.js';
import type { GeneralActionContext } from '../triggers/general.js';
import { getNextTurnAt, type TurnSchedule } from '../turn/calendar.js';
import {
LogCategory,
type LogEntryDraft,
LogFormat,
LogScope,
} from '../logging/types.js';
export interface GeneralActionResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends GeneralActionContext<TriggerState> {
rng: RandomGenerator;
city?: City;
nation?: Nation | null;
}
export interface TurnScheduleContext {
now: Date;
schedule: TurnSchedule;
}
export interface GeneralPatchEffect<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
type: 'general:patch';
patch: Partial<General<TriggerState>>;
targetId?: GeneralId;
}
export interface CityPatchEffect {
type: 'city:patch';
patch: Partial<City>;
targetId?: CityId;
}
export interface NationPatchEffect {
type: 'nation:patch';
patch: Partial<Nation>;
targetId?: NationId;
}
export interface LogEffect {
type: 'log';
entry: LogEntryDraft;
}
export interface NextTurnOverrideEffect {
type: 'schedule:override';
nextTurnAt: Date;
}
export type GeneralActionEffect<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> =
| GeneralPatchEffect<TriggerState>
| CityPatchEffect
| NationPatchEffect
| LogEffect
| NextTurnOverrideEffect;
export interface GeneralActionOutcome<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
effects: GeneralActionEffect<TriggerState>[];
}
export interface GeneralActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
Args = unknown
> {
key: string;
resolve(
context: GeneralActionResolveContext<TriggerState>,
args: Args
): GeneralActionOutcome<TriggerState>;
}
export interface GeneralActionResolution {
general: General;
city?: City;
nation?: Nation | null;
nextTurnAt: Date;
logs: LogEntryDraft[];
effects: GeneralActionEffect[];
patches?: {
generals: Array<{ id: GeneralId; patch: Partial<General> }>;
cities: Array<{ id: CityId; patch: Partial<City> }>;
nations: Array<{ id: NationId; patch: Partial<Nation> }>;
};
dirty?: {
general: boolean;
city: boolean;
nation: boolean;
generalId?: GeneralId;
cityId?: CityId;
nationId?: NationId;
};
}
const mergeStats = (base: StatBlock, patch: Partial<StatBlock>): StatBlock => ({
leadership: patch.leadership ?? base.leadership,
strength: patch.strength ?? base.strength,
intelligence: patch.intelligence ?? base.intelligence,
});
const mergeRole = (
base: GeneralRole,
patch: Partial<GeneralRole>
): GeneralRole => ({
...base,
...patch,
items: {
...base.items,
...(patch.items ?? {}),
},
});
const mergeTriggerState = <TriggerState extends GeneralTriggerState>(
base: TriggerState,
patch: Partial<TriggerState>
): TriggerState => ({
...base,
...patch,
flags: { ...base.flags, ...(patch.flags ?? {}) },
counters: { ...base.counters, ...(patch.counters ?? {}) },
modifiers: { ...base.modifiers, ...(patch.modifiers ?? {}) },
meta: { ...base.meta, ...(patch.meta ?? {}) },
});
const applyGeneralPatch = <TriggerState extends GeneralTriggerState>(
base: General<TriggerState>,
patch: Partial<General<TriggerState>>
): General<TriggerState> => ({
...base,
...patch,
stats: patch.stats ? mergeStats(base.stats, patch.stats) : base.stats,
role: patch.role ? mergeRole(base.role, patch.role) : base.role,
triggerState: patch.triggerState
? mergeTriggerState(base.triggerState, patch.triggerState)
: base.triggerState,
meta: patch.meta ? { ...base.meta, ...patch.meta } : base.meta,
});
const applyCityPatch = (base: City, patch: Partial<City>): City => ({
...base,
...patch,
meta: patch.meta ? { ...base.meta, ...patch.meta } : base.meta,
});
const applyNationPatch = (base: Nation, patch: Partial<Nation>): Nation => ({
...base,
...patch,
meta: patch.meta ? { ...base.meta, ...patch.meta } : base.meta,
});
export const createGeneralPatchEffect = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
patch: Partial<General<TriggerState>>,
targetId?: GeneralId
): GeneralPatchEffect<TriggerState> => ({
type: 'general:patch',
patch,
...(targetId !== undefined ? { targetId } : {}),
});
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 => ({
type: 'nation:patch',
patch,
...(targetId !== undefined ? { targetId } : {}),
});
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.userId !== undefined ? { userId: options.userId } : {}),
...(options.subType !== undefined ? { subType: options.subType } : {}),
...(options.meta !== undefined ? { meta: options.meta } : {}),
format: options.format ?? LogFormat.MONTH,
},
});
export const createNextTurnOverrideEffect = (
nextTurnAt: Date
): NextTurnOverrideEffect => ({
type: 'schedule:override',
nextTurnAt,
});
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
export const resolveGeneralAction = <
TriggerState extends GeneralTriggerState = GeneralTriggerState,
Args = unknown
>(
resolver: GeneralActionResolver<TriggerState, Args>,
context: GeneralActionResolveContext<TriggerState>,
scheduleContext: TurnScheduleContext,
args: Args
): GeneralActionResolution => {
const outcome = resolver.resolve(context, args);
const logs: LogEntryDraft[] = [];
let nextGeneral = context.general;
let nextCity = context.city;
let nextNation = context.nation ?? null;
let nextTurnAtOverride: Date | null = null;
const patches: NonNullable<GeneralActionResolution['patches']> = {
generals: [],
cities: [],
nations: [],
};
const dirty: NonNullable<GeneralActionResolution['dirty']> = {
general: false,
city: false,
nation: false,
generalId: context.general.id,
};
if (context.city) {
dirty.cityId = context.city.id;
}
if (context.nation) {
dirty.nationId = context.nation.id;
}
for (const effect of outcome.effects) {
switch (effect.type) {
case 'general:patch':
if (
effect.targetId === undefined ||
effect.targetId === context.general.id
) {
nextGeneral = applyGeneralPatch(nextGeneral, effect.patch);
dirty.general = true;
} else {
patches.generals.push({
id: effect.targetId,
patch: effect.patch as Partial<General>,
});
}
break;
case 'city:patch':
if (
effect.targetId === undefined ||
effect.targetId === nextCity?.id
) {
if (nextCity) {
nextCity = applyCityPatch(nextCity, effect.patch);
dirty.city = true;
}
} else {
patches.cities.push({
id: effect.targetId,
patch: effect.patch,
});
}
break;
case 'nation:patch':
if (
effect.targetId === undefined ||
effect.targetId === nextNation?.id
) {
if (nextNation) {
nextNation = applyNationPatch(nextNation, effect.patch);
dirty.nation = true;
}
} else {
patches.nations.push({
id: effect.targetId,
patch: effect.patch,
});
}
break;
case 'log':
// 로그 대상이 비어 있으면 현재 장수/국가 기준으로 보정한다.
switch (effect.entry.scope) {
case LogScope.GENERAL:
logs.push({
...effect.entry,
generalId:
effect.entry.generalId ?? context.general.id,
});
break;
case LogScope.NATION:
if (effect.entry.nationId !== undefined) {
logs.push(effect.entry);
break;
}
if (context.nation?.id !== undefined) {
logs.push({
...effect.entry,
nationId: context.nation.id,
});
}
break;
case LogScope.USER:
if (effect.entry.userId) {
logs.push(effect.entry);
}
break;
case LogScope.SYSTEM:
default:
logs.push(effect.entry);
break;
}
break;
case 'schedule:override':
nextTurnAtOverride = effect.nextTurnAt;
break;
default:
break;
}
}
const nextTurnAt =
nextTurnAtOverride ??
getNextTurnAt(scheduleContext.now, scheduleContext.schedule);
const resolution: GeneralActionResolution = {
general: nextGeneral,
nation: nextNation,
nextTurnAt,
logs,
effects: outcome.effects,
};
if (nextCity) {
resolution.city = nextCity;
}
if (dirty.general || dirty.city || dirty.nation) {
resolution.dirty = dirty;
}
if (
patches.generals.length > 0 ||
patches.cities.length > 0 ||
patches.nations.length > 0
) {
resolution.patches = patches;
}
return resolution;
};