Refactor action effects to use direct state mutations and logging

- Removed the use of createGeneralPatchEffect and createLogEffect in various action definitions.
- Replaced effects with direct mutations of the state for general and city entities.
- Updated logging to use context.addLog for better clarity and consistency.
- Ensured that all actions now return an empty effects array after processing.
This commit is contained in:
2026-01-02 10:25:33 +00:00
parent 18247eeff4
commit f137c5be65
17 changed files with 281 additions and 562 deletions
@@ -369,7 +369,7 @@ export const createReservedTurnHandler = async (options: {
const resolution = resolveGeneralAction(
definition,
actionContext as GeneralActionResolveContext,
actionContext,
{
now: currentGeneral.turnTime,
schedule: context.schedule,
+56 -144
View File
@@ -1,5 +1,5 @@
import type { RandomGenerator } from '@sammo-ts/common';
import { enablePatches, produceWithPatches, type Draft, castDraft } from 'immer';
import { enablePatches, produceWithPatches, castDraft } from 'immer';
import type {
City,
General,
@@ -34,8 +34,16 @@ export interface GeneralActionResolveContext<
rng: RandomGenerator;
city?: City;
nation?: Nation | null;
addLog(
message: string,
options?: Partial<Omit<LogEntryDraft, 'text'>>
): void;
}
export type GeneralActionResolveInputContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> = Omit<GeneralActionResolveContext<TriggerState>, 'addLog'>;
export interface TurnScheduleContext {
now: Date;
schedule: TurnSchedule;
@@ -130,106 +138,6 @@ export interface GeneralActionResolution {
};
}
/**
* Immer Draft에 Effect를 적용한다.
* 기존 Effect 기반 코드를 유지하면서 Draft에 즉시 반영하기 위함.
*/
export const applyEffectToDraft = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
draft: Draft<WorldState<TriggerState>>,
effect: GeneralActionEffect<TriggerState>,
context: { generalId: GeneralId; cityId?: CityId; nationId?: NationId }
): void => {
const generalDraft = draft.general as any;
switch (effect.type) {
case 'general:patch':
if (
effect.targetId === undefined ||
effect.targetId === context.generalId
) {
Object.assign(generalDraft, effect.patch);
if (effect.patch.stats) {
generalDraft.stats = {
...generalDraft.stats,
...effect.patch.stats,
};
}
if (effect.patch.role) {
generalDraft.role = {
...generalDraft.role,
...effect.patch.role,
items: {
...generalDraft.role.items,
...(effect.patch.role.items ?? {}),
},
};
}
if (effect.patch.triggerState) {
generalDraft.triggerState = {
...generalDraft.triggerState,
...effect.patch.triggerState,
flags: {
...generalDraft.triggerState.flags,
...(effect.patch.triggerState.flags ?? {}),
},
counters: {
...generalDraft.triggerState.counters,
...(effect.patch.triggerState.counters ?? {}),
},
modifiers: {
...generalDraft.triggerState.modifiers,
...(effect.patch.triggerState.modifiers ?? {}),
},
meta: {
...generalDraft.triggerState.meta,
...(effect.patch.triggerState.meta ?? {}),
},
};
}
if (effect.patch.meta) {
generalDraft.meta = {
...generalDraft.meta,
...effect.patch.meta,
};
}
}
break;
case 'city:patch':
if (
draft.city &&
(effect.targetId === undefined ||
effect.targetId === context.cityId)
) {
Object.assign(draft.city, effect.patch);
if (effect.patch.meta) {
draft.city.meta = {
...draft.city.meta,
...effect.patch.meta,
};
}
}
break;
case 'nation:patch':
if (
draft.nation &&
(effect.targetId === undefined ||
effect.targetId === context.nationId)
) {
Object.assign(draft.nation, effect.patch);
if (effect.patch.meta) {
draft.nation.meta = {
...draft.nation.meta,
...effect.patch.meta,
};
}
}
break;
default:
break;
}
};
export const createGeneralPatchEffect = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
@@ -303,7 +211,7 @@ export const resolveGeneralAction = <
Args = unknown
>(
resolver: GeneralActionResolver<TriggerState, Args>,
context: GeneralActionResolveContext<TriggerState>,
context: GeneralActionResolveInputContext<TriggerState>,
scheduleContext: TurnScheduleContext,
args: Args
): GeneralActionResolution => {
@@ -323,12 +231,56 @@ export const resolveGeneralAction = <
nation: context.nation,
} as WorldState<TriggerState>,
(draft) => {
const addLog = (
message: string,
options: Partial<Omit<LogEntryDraft, 'text'>> = {}
) => {
const entry: LogEntryDraft = {
scope: options.scope ?? LogScope.GENERAL,
category: options.category ?? LogCategory.ACTION,
text: message,
format: options.format ?? LogFormat.MONTH,
...options,
};
switch (entry.scope) {
case LogScope.GENERAL:
logs.push({
...entry,
generalId: entry.generalId ?? context.general.id,
});
break;
case LogScope.NATION:
if (entry.nationId !== undefined) {
logs.push(entry);
break;
}
if (context.nation?.id !== undefined) {
logs.push({
...entry,
nationId: context.nation.id,
});
}
break;
case LogScope.USER:
if (entry.userId) {
logs.push(entry);
}
break;
case LogScope.SYSTEM:
default:
logs.push(entry);
break;
}
};
const outcome = resolver.resolve(
{
...context,
general: castDraft(draft.general),
city: castDraft(draft.city),
nation: castDraft(draft.nation),
addLog,
} as GeneralActionResolveContext<TriggerState>,
args
);
@@ -336,38 +288,7 @@ export const resolveGeneralAction = <
for (const effect of outcome.effects) {
switch (effect.type) {
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;
}
addLog(effect.entry.text, effect.entry);
break;
case 'schedule:override':
nextTurnAtOverride = effect.nextTurnAt;
@@ -378,16 +299,7 @@ export const resolveGeneralAction = <
case 'general:patch':
case 'city:patch':
case 'nation:patch':
applyEffectToDraft(draft, effect, {
generalId: context.general.id,
...(context.city?.id !== undefined
? { cityId: context.city.id }
: {}),
...(context.nation?.id !== undefined
? { nationId: context.nation.id }
: {}),
});
// 타겟이 다른 경우 patches에 추가 (applyEffectToDraft에서 처리되지 않은 경우)
// 타겟이 다른 경우 patches에 추가
if (
effect.type === 'general:patch' &&
effect.targetId !== undefined &&
@@ -6,8 +6,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -35,24 +34,19 @@ export class ActionDefinition<
_args: UprisingArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
const meta = {
...general.meta,
// 직접 수정 (Immer Draft)
general.meta = {
...general.meta as object,
uprising: true as TriggerValue,
};
return {
effects: [
createGeneralPatchEffect<TriggerState>(
{ meta } as Partial<typeof general>,
general.id
),
createLogEffect(`${ACTION_NAME}을 준비했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog(`${ACTION_NAME}을 준비했습니다.`, {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
return { effects: [] };
}
}
@@ -6,8 +6,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -35,24 +34,19 @@ export class ActionDefinition<
_args: FoundingArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
const meta = {
...general.meta,
// 직접 수정 (Immer Draft)
general.meta = {
...general.meta as object,
founding: true as TriggerValue,
};
return {
effects: [
createGeneralPatchEffect<TriggerState>(
{ meta } as Partial<typeof general>,
general.id
),
createLogEffect(`${ACTION_NAME}을 준비했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog(`${ACTION_NAME}을 준비했습니다.`, {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
return { effects: [] };
}
}
@@ -1,5 +1,4 @@
import type {
General,
GeneralTriggerState,
Nation,
} from '../../../domain/entities.js';
@@ -20,12 +19,6 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import {
createGeneralPatchEffect,
createLogEffect,
createNationPatchEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -84,17 +77,10 @@ export class ActionDefinition<
_args: TechResearchArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
const nation = context.nation ?? null;
const nation = context.nation;
if (!nation) {
return {
effects: [
createLogEffect('국가 정보를 찾지 못했습니다.', {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog('국가 정보를 찾지 못했습니다.');
return { effects: [] };
}
const delta = this.env.techDelta ?? DEFAULT_TECH_DELTA;
@@ -107,26 +93,14 @@ export class ActionDefinition<
const nextTech = Math.min(currentTech + delta, maxTech);
const applied = nextTech - currentTech;
const costGold = this.env.costGold ?? 0;
const generalPatch: Partial<General<TriggerState>> = {
gold: Math.max(0, general.gold - costGold),
};
return {
effects: [
createNationPatchEffect(
{
meta: { ...nation.meta, tech: nextTech },
} as Partial<Nation>,
nation.id
),
createGeneralPatchEffect<TriggerState>(generalPatch, general.id),
createLogEffect(`${ACTION_NAME}로 기술이 ${applied} 상승했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
// 직접 수정 (Immer Draft)
nation.meta = { ...nation.meta, tech: nextTech };
general.gold = Math.max(0, general.gold - costGold);
context.addLog(`${ACTION_NAME}로 기술이 ${applied} 상승했습니다.`);
return { effects: [] };
}
}
@@ -1,5 +1,4 @@
import type {
General,
GeneralTriggerState,
} from '../../../domain/entities.js';
import type {
@@ -13,8 +12,6 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -74,21 +71,14 @@ export class ActionDefinition<
const nextAtmos = clamp(general.atmos + delta, 0, maxAtmos);
const applied = nextAtmos - general.atmos;
const costGold = this.env.costGold ?? 0;
const patch: Partial<General<TriggerState>> = {
atmos: nextAtmos,
gold: Math.max(0, general.gold - costGold),
};
return {
effects: [
createGeneralPatchEffect<TriggerState>(patch, general.id),
createLogEffect(`${ACTION_NAME}로 사기가 ${applied} 증가했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
// 직접 수정 (Immer Draft)
general.atmos = nextAtmos;
general.gold = Math.max(0, general.gold - costGold);
context.addLog(`${ACTION_NAME}로 사기가 ${applied} 증가했습니다.`);
return { effects: [] };
}
}
@@ -30,12 +30,6 @@ import type {
GeneralActionOutcome,
GeneralActionResolver,
GeneralActionResolveContext,
GeneralActionEffect,
} from '../../engine.js';
import {
createCityPatchEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -335,36 +329,24 @@ export class ActionResolver<
context.rng
);
const updatedCommerce = clamp(
// 직접 수정 (Immer Draft)
city.commerce = clamp(
city.commerce + result.score,
0,
city.commerceMax
);
const nextGold = Math.max(0, general.gold - result.costGold);
const nextRice = Math.max(0, general.rice - result.costRice);
const nextExperience = general.experience + result.exp;
const nextDedication = general.dedication + result.dedication;
general.gold = Math.max(0, general.gold - result.costGold);
general.rice = Math.max(0, general.rice - result.costRice);
general.experience += result.exp;
general.dedication += result.dedication;
const metaWithStatExp = addMetaNumber(general.meta, STAT_EXP_KEY, 1);
const metaUpdated =
general.meta =
result.pick === 'success'
? { ...metaWithStatExp, max_domestic_critical: result.score }
: { ...metaWithStatExp, max_domestic_critical: 0 };
const effects: Array<GeneralActionEffect<TriggerState>> = [
createCityPatchEffect({
[CITY_KEY]: updatedCommerce,
} as Partial<City>),
createGeneralPatchEffect({
gold: nextGold,
rice: nextRice,
experience: nextExperience,
dedication: nextDedication,
meta: metaUpdated,
}),
];
const pickLabel =
result.pick === 'success'
? '성공'
@@ -372,9 +354,9 @@ export class ActionResolver<
? '실패'
: '완료';
const logMessage = `${ACTION_NAME} ${pickLabel}: +${Math.round(result.score)}`;
effects.push(createLogEffect(logMessage));
context.addLog(logMessage);
return { effects };
return { effects: [] };
}
}
@@ -1,5 +1,4 @@
import type {
General,
GeneralTriggerState,
} from '../../../domain/entities.js';
import type {
@@ -13,8 +12,6 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -62,21 +59,14 @@ export class ActionDefinition<
const nextInjury = Math.max(0, general.injury - delta);
const applied = general.injury - nextInjury;
const costGold = this.env.costGold ?? 0;
const patch: Partial<General<TriggerState>> = {
injury: nextInjury,
gold: Math.max(0, general.gold - costGold),
};
return {
effects: [
createGeneralPatchEffect<TriggerState>(patch, general.id),
createLogEffect(`${ACTION_NAME}으로 부상이 ${applied} 회복되었습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
// 직접 수정 (Immer Draft)
general.injury = nextInjury;
general.gold = Math.max(0, general.gold - costGold);
context.addLog(`${ACTION_NAME}으로 부상이 ${applied} 회복되었습니다.`);
return { effects: [] };
}
}
@@ -27,9 +27,6 @@ import type {
} from '../../engine.js';
import {
createGeneralAddEffect,
createGeneralPatchEffect,
createLogEffect,
createNationPatchEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { buildRecruitmentGeneral } from './recruitment.js';
@@ -250,47 +247,34 @@ export class ActionResolver<
_args: VolunteerRecruitArgs
): GeneralActionOutcome<TriggerState> {
void _args;
const effects: Array<GeneralActionEffect<TriggerState>> = [];
const nation = context.nation ?? null;
const general = context.general;
const nation = context.nation;
const expGain = 5 * (DEFAULT_PRE_TURN + 1);
const dedGain = 5 * (DEFAULT_PRE_TURN + 1);
effects.push(
createGeneralPatchEffect({
experience: context.general.experience + expGain,
dedication: context.general.dedication + dedGain,
})
);
effects.push(
createLogEffect(`${ACTION_NAME} 발동!`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
})
);
effects.push(
createLogEffect(`${ACTION_NAME} 발동`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
})
);
// 직접 수정 (Immer Draft)
general.experience += expGain;
general.dedication += dedGain;
if (nation?.id) {
const generalName = context.general.name;
context.addLog(`${ACTION_NAME} 발동!`);
context.addLog(`${ACTION_NAME} 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
if (nation) {
const generalName = general.name;
const generalJosa = JosaUtil.pick(generalName, '이');
const actionJosa = JosaUtil.pick(ACTION_NAME, '을');
effects.push(
createLogEffect(
`<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,
}
);
}
@@ -310,16 +294,15 @@ export class ActionResolver<
const globalDelay = this.command.getGlobalDelay(context);
if (nation) {
effects.push(
createNationPatchEffect({
meta: {
gennum: nextGennum,
strategic_cmd_limit: globalDelay,
},
}, nation.id)
);
nation.meta = {
...nation.meta as object,
gennum: nextGennum,
strategic_cmd_limit: globalDelay,
};
}
const effects: Array<GeneralActionEffect<TriggerState>> = [];
const baseAge = this.env.npcAge ?? DEFAULT_NPC_AGE;
const deathYears = this.env.npcDeathYears ?? DEFAULT_NPC_DEATH_YEARS;
const killTurnMin = this.env.killTurnMin ?? DEFAULT_KILLTURN_MIN;
@@ -20,14 +20,11 @@ import {
} from '../../../triggers/general-action.js';
import type { GeneralActionDefinition } from '../../definition.js';
import type {
GeneralActionEffect,
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import {
createGeneralAddEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { buildRecruitmentGeneral } from './recruitment.js';
@@ -301,40 +298,35 @@ export class ActionResolver<
_args: TalentScoutArgs
): GeneralActionOutcome<TriggerState> {
void _args;
const general = context.general;
const { gold: reqGold, rice: reqRice } = this.command.getCost();
const prop = this.command.calcFoundProp(context);
const found = context.rng.nextBool(prop);
const statKey = pickStatExpKey(context.rng, context.general);
const statKey = pickStatExpKey(context.rng, general);
const metaAfter =
found
? addMetaNumber(context.general.meta, statKey, 3)
: addMetaNumber(context.general.meta, statKey, 1);
? addMetaNumber(general.meta, statKey, 3)
: addMetaNumber(general.meta, statKey, 1);
const nextGold = Math.max(0, context.general.gold - reqGold);
const nextRice = Math.max(0, context.general.rice - reqRice);
const nextGold = Math.max(0, general.gold - reqGold);
const nextRice = Math.max(0, general.rice - reqRice);
const expGain = found ? 200 : 100;
const dedGain = found ? 300 : 70;
const effects: Array<GeneralActionEffect<TriggerState>> = [
createGeneralPatchEffect({
gold: nextGold,
rice: nextRice,
experience: context.general.experience + expGain,
dedication: context.general.dedication + dedGain,
meta: metaAfter,
}),
];
// 직접 수정 (Immer Draft)
general.gold = nextGold;
general.rice = nextRice;
general.experience += expGain;
general.dedication += dedGain;
general.meta = metaAfter;
if (!found) {
effects.push(
createLogEffect('인재를 찾을 수 없었습니다.', {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
})
);
return { effects };
context.addLog('인재를 찾을 수 없었습니다.', {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
return { effects: [] };
}
const candidate = resolveCandidate(context, context.rng, this.env);
@@ -396,32 +388,25 @@ export class ActionResolver<
meta,
});
effects.push(createGeneralAddEffect(newGeneral));
const nameObjJosa = JosaUtil.pick(name, '을');
const nameSubjJosa = JosaUtil.pick(name, '이');
effects.push(
createLogEffect(`인재 <Y>${name}</>${nameObjJosa} 발견했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
})
);
effects.push(
createLogEffect(`인재 <Y>${name}</>${nameSubjJosa} 등장했습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
})
);
effects.push(
createLogEffect(`인재 <Y>${name}</>${nameObjJosa} 발견했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
})
);
context.addLog(`인재 <Y>${name}</>${nameObjJosa} 발견했습니다.`, {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
context.addLog(`인재 <Y>${name}</>${nameSubjJosa} 등장했습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
});
context.addLog(`인재 <Y>${name}</>${nameObjJosa} 발견했습니다.`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
return { effects };
return {
effects: [createGeneralAddEffect(newGeneral)],
};
}
}
@@ -9,8 +9,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -47,22 +46,17 @@ export class ActionDefinition<
}
resolve(
_context: GeneralActionResolveContext<TriggerState>,
context: GeneralActionResolveContext<TriggerState>,
args: AppointmentArgs
): GeneralActionOutcome<TriggerState> {
void _context;
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
],
};
context.addLog(
`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`,
{
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
);
return { effects: [] };
}
}
@@ -26,15 +26,9 @@ import {
} from '../../../triggers/general-action.js';
import type { GeneralActionDefinition } from '../../definition.js';
import type {
GeneralActionEffect,
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import {
createCityPatchEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
import type { MapDefinition, UnitSetDefinition } from '../../../world/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -351,16 +345,14 @@ export class ActionResolver<
): GeneralActionOutcome<TriggerState> {
const { general, city } = context;
if (!city) {
return {
effects: [createLogEffect('도시 정보가 없습니다.')],
};
context.addLog('도시 정보가 없습니다.');
return { effects: [] };
}
const crewType = findCrewTypeById(context.unitSet, args.crewType);
if (!crewType) {
return {
effects: [createLogEffect('병종 정보가 없습니다.')],
};
context.addLog('병종 정보가 없습니다.');
return { effects: [] };
}
const availabilityContext: CrewTypeAvailabilityContext = {
@@ -376,9 +368,8 @@ export class ActionResolver<
availabilityContext.startYear = context.startYear;
}
if (!isCrewTypeAvailable(context.unitSet, crewType.id, availabilityContext)) {
return {
effects: [createLogEffect('현재 선택할 수 없는 병종입니다.')],
};
context.addLog('현재 선택할 수 없는 병종입니다.');
return { effects: [] };
}
const plan = this.command.getCost(
@@ -435,30 +426,26 @@ export class ActionResolver<
const expGain = Math.round(appliedCrew / 100);
const dedGain = Math.round(appliedCrew / 100);
const metaUpdated = addMetaNumber(general.meta, 'leadership_exp', 1);
// 직접 수정 (Immer Draft)
city.population = nextPopulation;
city.meta = {
...city.meta as object,
trust: nextTrust,
};
const effects: Array<GeneralActionEffect<TriggerState>> = [
createCityPatchEffect({
population: nextPopulation,
meta: {
trust: nextTrust,
},
}),
createGeneralPatchEffect({
crewTypeId: nextCrewTypeId,
crew: nextCrew,
train: nextTrain,
atmos: nextAtmos,
gold: nextGold,
rice: nextRice,
experience: general.experience + expGain,
dedication: general.dedication + dedGain,
meta: metaUpdated,
}),
createLogEffect(logMessage),
];
general.crewTypeId = nextCrewTypeId;
general.crew = nextCrew;
general.train = nextTrain;
general.atmos = nextAtmos;
general.gold = nextGold;
general.rice = nextRice;
general.experience += expGain;
general.dedication += dedGain;
general.meta = addMetaNumber(general.meta, 'leadership_exp', 1);
return { effects };
context.addLog(logMessage);
return { effects: [] };
}
}
@@ -12,8 +12,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -56,19 +55,14 @@ export class ActionDefinition<
}
resolve(
_context: GeneralActionResolveContext<TriggerState>,
context: GeneralActionResolveContext<TriggerState>,
args: DispatchArgs
): GeneralActionOutcome<TriggerState> {
void _context;
return {
effects: [
createLogEffect(`${ACTION_NAME}을 준비했습니다. (목표 도시 ${args.destCityId})`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog(`${ACTION_NAME}을 준비했습니다. (목표 도시 ${args.destCityId})`, {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
return { effects: [] };
}
}
@@ -33,7 +33,6 @@ import type {
import {
createCityPatchEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
@@ -332,6 +331,7 @@ export class ActionResolver<
_args: FireAttackArgs
): GeneralActionOutcome<TriggerState> {
void _args;
const general = context.general;
const city = context.city;
if (!city) {
throw new Error('Fire attack requires a city context.');
@@ -351,13 +351,13 @@ export class ActionResolver<
const effects: Array<GeneralActionEffect<TriggerState>> = [];
const nextGold = Math.max(0, context.general.gold - result.costGold);
const nextRice = Math.max(0, context.general.rice - result.costRice);
const nextExperience = context.general.experience + result.exp;
const nextDedication = context.general.dedication + result.dedication;
const nextGold = Math.max(0, general.gold - result.costGold);
const nextRice = Math.max(0, general.rice - result.costRice);
const nextExperience = general.experience + result.exp;
const nextDedication = general.dedication + result.dedication;
const metaWithStatExp = addMetaNumber(
context.general.meta,
general.meta,
STAT_EXP_KEY,
1
);
@@ -365,26 +365,21 @@ export class ActionResolver<
? addMetaNumber(metaWithStatExp, 'firenum', 1)
: metaWithStatExp;
effects.push(
createGeneralPatchEffect({
gold: nextGold,
rice: nextRice,
experience: nextExperience,
dedication: nextDedication,
meta: metaUpdated,
})
);
// 직접 수정 (Immer Draft)
general.gold = nextGold;
general.rice = nextRice;
general.experience = nextExperience;
general.dedication = nextDedication;
general.meta = metaUpdated;
if (!result.success) {
effects.push(
createLogEffect(
`<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 };
return { effects: [] };
}
const updatedCityMeta: Record<string, TriggerValue> = {
@@ -392,6 +387,7 @@ export class ActionResolver<
state: CITY_STATE_BURNING,
};
// 타겟 도시는 Draft가 아니므로 Effect 반환
effects.push(
createCityPatchEffect(
{
@@ -403,45 +399,38 @@ export class ActionResolver<
)
);
effects.push(
createLogEffect(
`<G><b>${context.destCity.name}</b></>이 불타고 있습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
)
context.addLog(
`<G><b>${context.destCity.name}</b></>이 불타고 있습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
);
effects.push(
createLogEffect(
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 성공했습니다.`,
{
format: LogFormat.MONTH,
}
)
context.addLog(
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 성공했습니다.`,
{
format: LogFormat.MONTH,
}
);
effects.push(
createLogEffect(
`도시의 농업이 <C>${result.agriDamage}</>, 상업이 <C>${result.commDamage}</>만큼 감소하고, 장수 <C>${result.injuryCount}</>명이 부상 당했습니다.`,
{
format: LogFormat.PLAIN,
}
)
context.addLog(
`도시의 농업이 <C>${result.agriDamage}</>, 상업이 <C>${result.commDamage}</>만큼 감소하고, 장수 <C>${result.injuryCount}</>명이 부상 당했습니다.`,
{
format: LogFormat.PLAIN,
}
);
for (const injured of result.injuredGenerals) {
// 타겟 장수는 Draft가 아니므로 Effect 반환
effects.push(
createGeneralPatchEffect(injured.patch, injured.id)
);
effects.push(
createLogEffect(
`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`,
{
generalId: injured.id,
format: LogFormat.MONTH,
}
)
context.addLog(
`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`,
{
generalId: injured.id,
format: LogFormat.MONTH,
}
);
}
@@ -1,5 +1,4 @@
import type {
General,
GeneralTriggerState,
} from '../../../domain/entities.js';
import type {
@@ -13,8 +12,6 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -74,21 +71,14 @@ export class ActionDefinition<
const nextTrain = clamp(general.train + delta, 0, maxTrain);
const applied = nextTrain - general.train;
const costGold = this.env.costGold ?? 0;
const patch: Partial<General<TriggerState>> = {
train: nextTrain,
gold: Math.max(0, general.gold - costGold),
};
return {
effects: [
createGeneralPatchEffect<TriggerState>(patch, general.id),
createLogEffect(`${ACTION_NAME}을 통해 훈련도가 ${applied} 증가했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
// 직접 수정 (Immer Draft)
general.train = nextTrain;
general.gold = Math.max(0, general.gold - costGold);
context.addLog(`${ACTION_NAME}을 통해 훈련도가 ${applied} 증가했습니다.`);
return { effects: [] };
}
}
@@ -1,6 +1,5 @@
import type {
City,
General,
GeneralTriggerState,
} from '../../../domain/entities.js';
import type {
@@ -21,12 +20,6 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import {
createCityPatchEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
export interface CityDevelopmentArgs {}
@@ -98,53 +91,28 @@ export class CityDevelopmentActionDefinition<
const general = context.general;
const city = context.city;
if (!city) {
return {
effects: [
createLogEffect('도시 정보를 찾지 못했습니다.', {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog('도시 정보를 찾지 못했습니다.');
return { effects: [] };
}
const baseAmount = this.env.amount ?? this.config.baseAmount;
const current = readNumber(city[this.config.statKey]);
const max = readNumber(city[this.config.maxKey]);
if (current === null || max === null) {
return {
effects: [
createLogEffect('도시 정보를 찾지 못했습니다.', {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog('도시 정보를 찾지 못했습니다.');
return { effects: [] };
}
const nextValue = clamp(current + baseAmount, 0, max);
const costGold = this.env.develCost ?? 0;
const generalPatch: Partial<General<TriggerState>> = {
gold: Math.max(0, general.gold - costGold),
};
// 직접 수정 (Immer Draft)
(city as any)[this.config.statKey] = nextValue;
general.gold = Math.max(0, general.gold - costGold);
const logMessage = `${this.config.label}${nextValue - current} 증가했습니다.`;
context.addLog(logMessage);
return {
effects: [
createCityPatchEffect(
{ [this.config.statKey]: nextValue } as Partial<City>,
city.id
),
createGeneralPatchEffect<TriggerState>(generalPatch, general.id),
createLogEffect(logMessage, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
return { effects: [] };
}
}
@@ -10,8 +10,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '../../engine.js';
import { createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -23,20 +22,14 @@ export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
resolve(
_context: GeneralActionResolveContext<TriggerState>,
context: GeneralActionResolveContext<TriggerState>,
_args: RestArgs
): GeneralActionOutcome<TriggerState> {
void _context;
void _args;
return {
effects: [
createLogEffect('아무것도 실행하지 않았습니다.', {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
context.addLog('아무것도 실행하지 않았습니다.', {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
return { effects: [] };
}
}