feat: 상업 투자 관련 로직 및 효과 추가, 엔진에서 더러운 상태 추적 기능 개선
This commit is contained in:
@@ -10,6 +10,17 @@ import {
|
|||||||
GeneralActionPipeline,
|
GeneralActionPipeline,
|
||||||
type GeneralActionModule,
|
type GeneralActionModule,
|
||||||
} from '../../triggers/general-action.js';
|
} 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';
|
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
||||||
|
|
||||||
@@ -51,6 +62,9 @@ export interface CommerceInvestmentResult {
|
|||||||
const DEFAULT_TRUST = 50;
|
const DEFAULT_TRUST = 50;
|
||||||
const DEFAULT_FRONT_DEBUFF = 0.5;
|
const DEFAULT_FRONT_DEBUFF = 0.5;
|
||||||
const DEFAULT_FRONT_STATES = [1, 3];
|
const DEFAULT_FRONT_STATES = [1, 3];
|
||||||
|
const ACTION_NAME = '상업 투자';
|
||||||
|
const CITY_KEY = 'commerce';
|
||||||
|
const STAT_EXP_KEY = 'intel_exp';
|
||||||
|
|
||||||
const getMetaNumber = (
|
const getMetaNumber = (
|
||||||
meta: Record<string, unknown>,
|
meta: Record<string, unknown>,
|
||||||
@@ -85,6 +99,15 @@ const pickByWeight = (
|
|||||||
return 'normal';
|
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<
|
export class CommandResolver<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
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,
|
General,
|
||||||
GeneralRole,
|
GeneralRole,
|
||||||
GeneralTriggerState,
|
GeneralTriggerState,
|
||||||
|
CityId,
|
||||||
|
GeneralId,
|
||||||
Nation,
|
Nation,
|
||||||
|
NationId,
|
||||||
StatBlock,
|
StatBlock,
|
||||||
} from '../domain/entities.js';
|
} from '../domain/entities.js';
|
||||||
import type { GeneralActionContext } from '../triggers/general.js';
|
import type { GeneralActionContext } from '../triggers/general.js';
|
||||||
@@ -81,6 +84,14 @@ export interface GeneralActionResolution {
|
|||||||
nextTurnAt: Date;
|
nextTurnAt: Date;
|
||||||
logs: string[];
|
logs: string[];
|
||||||
effects: GeneralActionEffect[];
|
effects: GeneralActionEffect[];
|
||||||
|
dirty?: {
|
||||||
|
general: boolean;
|
||||||
|
city: boolean;
|
||||||
|
nation: boolean;
|
||||||
|
generalId?: GeneralId;
|
||||||
|
cityId?: CityId;
|
||||||
|
nationId?: NationId;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeStats = (base: StatBlock, patch: Partial<StatBlock>): StatBlock => ({
|
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,
|
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로 모아 상태/턴 계산을 수행한다.
|
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
|
||||||
export const resolveGeneralAction = <
|
export const resolveGeneralAction = <
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -153,20 +197,35 @@ export const resolveGeneralAction = <
|
|||||||
let nextCity = context.city;
|
let nextCity = context.city;
|
||||||
let nextNation = context.nation ?? null;
|
let nextNation = context.nation ?? null;
|
||||||
let nextTurnAtOverride: Date | null = 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) {
|
for (const effect of outcome.effects) {
|
||||||
switch (effect.type) {
|
switch (effect.type) {
|
||||||
case 'general:patch':
|
case 'general:patch':
|
||||||
nextGeneral = applyGeneralPatch(nextGeneral, effect.patch);
|
nextGeneral = applyGeneralPatch(nextGeneral, effect.patch);
|
||||||
|
dirty.general = true;
|
||||||
break;
|
break;
|
||||||
case 'city:patch':
|
case 'city:patch':
|
||||||
if (nextCity) {
|
if (nextCity) {
|
||||||
nextCity = applyCityPatch(nextCity, effect.patch);
|
nextCity = applyCityPatch(nextCity, effect.patch);
|
||||||
|
dirty.city = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'nation:patch':
|
case 'nation:patch':
|
||||||
if (nextNation) {
|
if (nextNation) {
|
||||||
nextNation = applyNationPatch(nextNation, effect.patch);
|
nextNation = applyNationPatch(nextNation, effect.patch);
|
||||||
|
dirty.nation = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'log':
|
case 'log':
|
||||||
@@ -194,6 +253,9 @@ export const resolveGeneralAction = <
|
|||||||
if (nextCity) {
|
if (nextCity) {
|
||||||
resolution.city = nextCity;
|
resolution.city = nextCity;
|
||||||
}
|
}
|
||||||
|
if (dirty.general || dirty.city || dirty.nation) {
|
||||||
|
resolution.dirty = dirty;
|
||||||
|
}
|
||||||
|
|
||||||
return resolution;
|
return resolution;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user