feat: 상업 투자 관련 로직 및 효과 추가, 엔진에서 더러운 상태 추적 기능 개선
This commit is contained in:
@@ -10,6 +10,17 @@ import {
|
||||
GeneralActionPipeline,
|
||||
type GeneralActionModule,
|
||||
} from '../../triggers/general-action.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolver,
|
||||
GeneralActionResolveContext,
|
||||
GeneralActionEffect,
|
||||
} from '../engine.js';
|
||||
import {
|
||||
createCityPatchEffect,
|
||||
createGeneralPatchEffect,
|
||||
createLogEffect,
|
||||
} from '../engine.js';
|
||||
|
||||
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
||||
|
||||
@@ -51,6 +62,9 @@ export interface CommerceInvestmentResult {
|
||||
const DEFAULT_TRUST = 50;
|
||||
const DEFAULT_FRONT_DEBUFF = 0.5;
|
||||
const DEFAULT_FRONT_STATES = [1, 3];
|
||||
const ACTION_NAME = '상업 투자';
|
||||
const CITY_KEY = 'commerce';
|
||||
const STAT_EXP_KEY = 'intel_exp';
|
||||
|
||||
const getMetaNumber = (
|
||||
meta: Record<string, unknown>,
|
||||
@@ -85,6 +99,15 @@ const pickByWeight = (
|
||||
return 'normal';
|
||||
};
|
||||
|
||||
const addMetaNumber = (
|
||||
meta: Record<string, unknown>,
|
||||
key: string,
|
||||
delta: number
|
||||
): Record<string, unknown> => {
|
||||
const current = getMetaNumber(meta, key) ?? 0;
|
||||
return { ...meta, [key]: current + delta };
|
||||
};
|
||||
|
||||
// 상업 투자 결과치를 계산하는 경로를 제공한다.
|
||||
export class CommandResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
@@ -226,3 +249,77 @@ export class CommandResolver<
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionResolver<TriggerState> {
|
||||
readonly key = 'che_상업투자';
|
||||
private readonly command: CommandResolver<TriggerState>;
|
||||
|
||||
constructor(
|
||||
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
|
||||
env: InvestmentEnvironment
|
||||
) {
|
||||
this.command = new CommandResolver(modules, env);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
const city = context.city;
|
||||
if (!city) {
|
||||
throw new Error('Commerce investment requires a city context.');
|
||||
}
|
||||
|
||||
const result = this.command.resolve(
|
||||
{
|
||||
...context,
|
||||
city,
|
||||
nation: context.nation ?? null,
|
||||
},
|
||||
context.rng
|
||||
);
|
||||
|
||||
const updatedCommerce = 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;
|
||||
|
||||
const metaWithStatExp = addMetaNumber(general.meta, STAT_EXP_KEY, 1);
|
||||
const metaUpdated =
|
||||
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'
|
||||
? '성공'
|
||||
: result.pick === 'fail'
|
||||
? '실패'
|
||||
: '완료';
|
||||
const logMessage = `${ACTION_NAME} ${pickLabel}: +${Math.round(result.score)}`;
|
||||
effects.push(createLogEffect(logMessage));
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import type {
|
||||
General,
|
||||
GeneralRole,
|
||||
GeneralTriggerState,
|
||||
CityId,
|
||||
GeneralId,
|
||||
Nation,
|
||||
NationId,
|
||||
StatBlock,
|
||||
} from '../domain/entities.js';
|
||||
import type { GeneralActionContext } from '../triggers/general.js';
|
||||
@@ -81,6 +84,14 @@ export interface GeneralActionResolution {
|
||||
nextTurnAt: Date;
|
||||
logs: string[];
|
||||
effects: GeneralActionEffect[];
|
||||
dirty?: {
|
||||
general: boolean;
|
||||
city: boolean;
|
||||
nation: boolean;
|
||||
generalId?: GeneralId;
|
||||
cityId?: CityId;
|
||||
nationId?: NationId;
|
||||
};
|
||||
}
|
||||
|
||||
const mergeStats = (base: StatBlock, patch: Partial<StatBlock>): StatBlock => ({
|
||||
@@ -139,6 +150,39 @@ const applyNationPatch = (base: Nation, patch: Partial<Nation>): Nation => ({
|
||||
meta: patch.meta ? { ...base.meta, ...patch.meta } : base.meta,
|
||||
});
|
||||
|
||||
export const createGeneralPatchEffect = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
patch: Partial<General<TriggerState>>
|
||||
): GeneralPatchEffect<TriggerState> => ({
|
||||
type: 'general:patch',
|
||||
patch,
|
||||
});
|
||||
|
||||
export const createCityPatchEffect = (patch: Partial<City>): CityPatchEffect => ({
|
||||
type: 'city:patch',
|
||||
patch,
|
||||
});
|
||||
|
||||
export const createNationPatchEffect = (
|
||||
patch: Partial<Nation>
|
||||
): NationPatchEffect => ({
|
||||
type: 'nation:patch',
|
||||
patch,
|
||||
});
|
||||
|
||||
export const createLogEffect = (message: string): LogEffect => ({
|
||||
type: 'log',
|
||||
message,
|
||||
});
|
||||
|
||||
export const createNextTurnOverrideEffect = (
|
||||
nextTurnAt: Date
|
||||
): NextTurnOverrideEffect => ({
|
||||
type: 'schedule:override',
|
||||
nextTurnAt,
|
||||
});
|
||||
|
||||
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
|
||||
export const resolveGeneralAction = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
@@ -153,20 +197,35 @@ export const resolveGeneralAction = <
|
||||
let nextCity = context.city;
|
||||
let nextNation = context.nation ?? null;
|
||||
let nextTurnAtOverride: Date | null = null;
|
||||
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':
|
||||
nextGeneral = applyGeneralPatch(nextGeneral, effect.patch);
|
||||
dirty.general = true;
|
||||
break;
|
||||
case 'city:patch':
|
||||
if (nextCity) {
|
||||
nextCity = applyCityPatch(nextCity, effect.patch);
|
||||
dirty.city = true;
|
||||
}
|
||||
break;
|
||||
case 'nation:patch':
|
||||
if (nextNation) {
|
||||
nextNation = applyNationPatch(nextNation, effect.patch);
|
||||
dirty.nation = true;
|
||||
}
|
||||
break;
|
||||
case 'log':
|
||||
@@ -194,6 +253,9 @@ export const resolveGeneralAction = <
|
||||
if (nextCity) {
|
||||
resolution.city = nextCity;
|
||||
}
|
||||
if (dirty.general || dirty.city || dirty.nation) {
|
||||
resolution.dirty = dirty;
|
||||
}
|
||||
|
||||
return resolution;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user