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.
This commit is contained in:
2025-12-29 09:38:32 +00:00
parent 27cc016804
commit ee719df8fe
16 changed files with 1301 additions and 66 deletions
@@ -0,0 +1,439 @@
import type { RandomGenerator } from '@sammo-ts/common';
import type {
City,
General,
GeneralTriggerState,
Nation,
} from '../../../domain/entities.js';
import type {
Constraint,
ConstraintContext,
RequirementKey,
StateView,
} from '../../../constraints/types.js';
import {
notBeNeutral,
notWanderingNation,
occupiedCity,
remainCityCapacity,
reqGeneralGold,
reqGeneralRice,
suppliedCity,
} from '../../../constraints/presets.js';
import type { GeneralActionContext } from '../../../triggers/general.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '../../../triggers/general-action.js';
import type { GeneralActionDefinition } from '../../definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolver,
GeneralActionResolveContext,
GeneralActionEffect,
} from '../../engine.js';
import {
createCityPatchEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
export interface DomesticActionContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends GeneralActionContext<TriggerState> {
general: General<TriggerState>;
city: City;
nation?: Nation | null;
}
export interface InvestmentEnvironment {
develCost: number;
defaultTrust?: number;
frontDebuff?: number;
frontStatesWithDebuff?: number[];
getDomesticExpLevelBonus?: (expLevel: number) => number;
getCriticalRatio?: (
context: DomesticActionContext,
statKey: string
) => { success: number; fail: number };
getCriticalScoreMultiplier?: (
rng: RandomGenerator,
pick: DomesticCriticalPick
) => number;
adjustFrontDebuff?: (context: DomesticActionContext, debuff: number) => number;
}
export interface CommerceInvestmentResult {
pick: DomesticCriticalPick;
score: number;
exp: number;
dedication: number;
costGold: number;
costRice: number;
appliedFrontDebuff: boolean;
}
export interface CommerceInvestmentArgs {}
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>,
key: string
): number | null => {
const raw = meta[key];
return typeof raw === 'number' ? raw : null;
};
const clamp = (value: number, min: number, max: number): number =>
Math.min(Math.max(value, min), max);
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;
if (total <= 0) {
return 'normal';
}
let cursor = rng.nextFloat() * total;
for (const key of ['fail', 'normal', 'success'] as const) {
cursor -= weights[key];
if (cursor <= 0) {
return key;
}
}
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 };
};
const buildDomesticContextFromView = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
ctx: ConstraintContext,
view: StateView
): DomesticActionContext<TriggerState> | null => {
const general = view.get({
kind: 'general',
id: ctx.actorId,
}) as General<TriggerState> | null;
if (!general) {
return null;
}
const cityId = ctx.cityId ?? general.cityId;
const city = view.get({ kind: 'city', id: cityId }) as City | null;
if (!city) {
return null;
}
const nationId = ctx.nationId ?? general.nationId;
const nation =
nationId !== undefined
? ((view.get({ kind: 'nation', id: nationId }) as Nation | null) ??
null)
: null;
return {
general,
city,
nation,
};
};
// 상업 투자 결과치를 계산하는 경로를 제공한다.
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
) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
}
getCost(context: DomesticActionContext<TriggerState>): {
gold: number;
rice: number;
} {
const baseGold = this.env.develCost;
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;
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;
score *= trust / 100;
score *= expBonus;
score *= randomRange(rng, 0.8, 1.2);
return this.pipeline.onCalcDomestic(
context,
this.actionKey,
'score',
score
);
}
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;
let score = clamp(this.calcBaseScore(context, rng), 1, Number.MAX_SAFE_INTEGER);
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 = clamp(successRatio, 0, 1);
failRatio = clamp(failRatio, 0, 1 - successRatio);
const normalRatio = 1 - successRatio - failRatio;
const pick = pickByWeight(rng, {
fail: failRatio,
success: successRatio,
normal: normalRatio,
});
const criticalMultiplier =
this.env.getCriticalScoreMultiplier?.(rng, pick) ?? 1;
score = Math.round(score * criticalMultiplier);
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;
score *= adjustedDebuff;
appliedFrontDebuff = true;
}
const exp = score * 0.7;
const dedication = score * 1.0;
return {
pick,
score,
exp,
dedication,
costGold,
costRice,
appliedFrontDebuff,
};
}
}
export class ActionResolver<
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
) {
this.command = new CommandResolver(modules, env);
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
_args: CommerceInvestmentArgs
): GeneralActionOutcome<TriggerState> {
void _args;
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 };
}
}
export class ActionDefinition<
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
) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
parseArgs(_raw: unknown): CommerceInvestmentArgs | null {
void _raw;
return {};
}
buildConstraints(
ctx: ConstraintContext,
_args: CommerceInvestmentArgs
): Constraint[] {
void _args;
const requirements: RequirementKey[] = [];
if (ctx.cityId !== undefined) {
requirements.push({ kind: 'city', id: ctx.cityId });
}
if (ctx.nationId !== undefined) {
requirements.push({ kind: 'nation', id: ctx.nationId });
}
const getCost = (context: ConstraintContext, view: StateView): number => {
const domesticContext =
buildDomesticContextFromView<TriggerState>(context, view);
if (!domesticContext) {
return 0;
}
return this.command.getCost(domesticContext).gold;
};
return [
notBeNeutral(),
notWanderingNation(),
occupiedCity(),
suppliedCity(),
reqGeneralGold(getCost, requirements),
reqGeneralRice(() => 0, requirements),
remainCityCapacity(CITY_KEY, ACTION_NAME),
];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
args: CommerceInvestmentArgs
): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -0,0 +1,505 @@
import type { RandomGenerator } from '@sammo-ts/common';
import type {
City,
General,
GeneralTriggerState,
Nation,
TriggerValue,
} from '../../../domain/entities.js';
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
import {
disallowDiplomacyBetweenStatus,
existsDestCity,
notBeNeutral,
notNeutralDestCity,
notOccupiedDestCity,
occupiedCity,
reqGeneralGold,
reqGeneralRice,
suppliedCity,
} from '../../../constraints/presets.js';
import type { GeneralActionContext } from '../../../triggers/general.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '../../../triggers/general-action.js';
import type { GeneralActionDefinition } from '../../definition.js';
import type {
GeneralActionEffect,
GeneralActionOutcome,
GeneralActionResolveContext,
GeneralActionResolver,
} from '../../engine.js';
import {
createCityPatchEffect,
createGeneralPatchEffect,
createLogEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
export interface FireAttackArgs {
destCityId: number;
}
export interface FireAttackEnvironment {
develCost: number;
sabotageDefaultProb: number;
sabotageProbCoefByStat: number;
sabotageDefenceCoefByGeneralCount: number;
sabotageDamageMin: number;
sabotageDamageMax: number;
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;
}
export interface FireAttackContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends GeneralActionContext<TriggerState> {
general: General<TriggerState>;
city: City;
nation?: Nation | null;
destCity: City;
destNation?: Nation | null;
destGenerals: General<TriggerState>[];
}
export interface FireAttackResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends GeneralActionResolveContext<TriggerState> {
destCity: City;
destNation?: Nation | null;
destGenerals: General<TriggerState>[];
}
export interface FireAttackResult<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
success: boolean;
probability: number;
distance: number;
costGold: number;
costRice: number;
exp: number;
dedication: number;
agriDamage: number;
commDamage: number;
injuryCount: number;
injuredGenerals: Array<{
id: number;
patch: Partial<General<TriggerState>>;
}>;
}
const ACTION_NAME = '화계';
const ACTION_KEY = '계략';
const STAT_EXP_KEY = 'intel_exp';
const DEFAULT_MAX_PROB = 0.5;
const INJURY_MAX = 80;
const CITY_STATE_BURNING = 32;
const clamp = (value: number, min: number, max: number): number =>
Math.min(Math.max(value, min), max);
const randomRangeInt = (
rng: RandomGenerator,
min: number,
max: number
): number => rng.nextInt(min, max + 1);
const getStatValue = (
general: General,
statKey: 'leadership' | 'strength' | 'intelligence'
): number => {
if (statKey === 'leadership') {
return general.stats.leadership;
}
if (statKey === 'strength') {
return general.stats.strength;
}
return general.stats.intelligence;
};
const addMetaNumber = (
meta: Record<string, TriggerValue>,
key: string,
delta: number
): Record<string, TriggerValue> => {
const current = typeof meta[key] === 'number' ? (meta[key] as number) : 0;
return { ...meta, [key]: current + delta };
};
// 화계 성공/실패 및 피해량 계산을 담당한다.
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
) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
this.statKey = env.statKey ?? 'intelligence';
}
getCost(): { gold: number; rice: number } {
const cost = this.env.develCost * 5;
return { gold: cost, rice: cost };
}
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
);
}
private calcDefenceProb(
context: FireAttackContext<TriggerState>
): number {
const destNationId = context.destCity.nationId;
let maxStat = 0;
let probCorrection = 0;
let affectCount = 0;
for (const defender of context.destGenerals) {
if (defender.nationId !== destNationId) {
continue;
}
affectCount += 1;
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 += context.destCity.security / context.destCity.securityMax / 5;
prob += context.destCity.supplyState ? 0.1 : 0;
return prob;
}
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 attackProb = this.calcAttackProb(context);
const defenceProb = this.calcDefenceProb(context);
let probability =
this.env.sabotageDefaultProb + attackProb - defenceProb;
probability /= distance;
probability = clamp(
probability,
0,
this.env.maxSuccessProbability ?? DEFAULT_MAX_PROB
);
const success = rng.nextBool(probability);
const expRange: [number, number] = success ? [201, 300] : [1, 100];
const dedRange: [number, number] = success ? [141, 210] : [1, 70];
const exp = randomRangeInt(rng, expRange[0], expRange[1]);
const dedication = randomRangeInt(rng, dedRange[0], dedRange[1]);
if (!success) {
return {
success,
probability,
distance,
costGold,
costRice,
exp,
dedication,
agriDamage: 0,
commDamage: 0,
injuryCount: 0,
injuredGenerals: [],
};
}
const agriDamage = clamp(
randomRangeInt(
rng,
this.env.sabotageDamageMin,
this.env.sabotageDamageMax
),
0,
context.destCity.agriculture
);
const commDamage = clamp(
randomRangeInt(
rng,
this.env.sabotageDamageMin,
this.env.sabotageDamageMax
),
0,
context.destCity.commerce
);
const injuryProbDefault = 0.3;
const injuredGenerals: Array<{
id: number;
patch: Partial<General<TriggerState>>;
}> = [];
for (const defender of context.destGenerals) {
if (defender.nationId !== context.destCity.nationId) {
continue;
}
const injuryProb =
this.env.getInjuryProbability?.(context, defender) ??
injuryProbDefault;
if (!rng.nextBool(injuryProb)) {
continue;
}
const injuryAmount = randomRangeInt(rng, 1, 16);
injuredGenerals.push({
id: defender.id,
patch: {
injury: clamp(
defender.injury + injuryAmount,
0,
INJURY_MAX
),
crew: Math.floor(defender.crew * 0.98),
train: Math.floor(defender.train * 0.98),
},
});
}
return {
success,
probability,
distance,
costGold,
costRice,
exp,
dedication,
agriDamage,
commDamage,
injuryCount: injuredGenerals.length,
injuredGenerals,
};
}
}
export class ActionResolver<
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
) {
this.command = new CommandResolver(modules, env);
}
resolve(
context: FireAttackResolveContext<TriggerState>,
_args: FireAttackArgs
): GeneralActionOutcome<TriggerState> {
void _args;
const city = context.city;
if (!city) {
throw new Error('Fire attack requires a city context.');
}
const result = this.command.resolve(
{
...context,
city,
nation: context.nation ?? null,
destCity: context.destCity,
destNation: context.destNation ?? null,
destGenerals: context.destGenerals,
},
context.rng
);
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 metaWithStatExp = addMetaNumber(
context.general.meta,
STAT_EXP_KEY,
1
);
const metaUpdated = result.success
? addMetaNumber(metaWithStatExp, 'firenum', 1)
: metaWithStatExp;
effects.push(
createGeneralPatchEffect({
gold: nextGold,
rice: nextRice,
experience: nextExperience,
dedication: nextDedication,
meta: metaUpdated,
})
);
if (!result.success) {
effects.push(
createLogEffect(
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 실패했습니다.`,
{
format: LogFormat.MONTH,
}
)
);
return { effects };
}
const updatedCityMeta: Record<string, TriggerValue> = {
...context.destCity.meta,
state: CITY_STATE_BURNING,
};
effects.push(
createCityPatchEffect(
{
agriculture: context.destCity.agriculture - result.agriDamage,
commerce: context.destCity.commerce - result.commDamage,
meta: updatedCityMeta,
},
context.destCity.id
)
);
effects.push(
createLogEffect(
`<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,
}
)
);
effects.push(
createLogEffect(
`도시의 농업이 <C>${result.agriDamage}</>, 상업이 <C>${result.commDamage}</>만큼 감소하고, 장수 <C>${result.injuryCount}</>명이 부상 당했습니다.`,
{
format: LogFormat.PLAIN,
}
)
);
for (const injured of result.injuredGenerals) {
effects.push(
createGeneralPatchEffect(injured.patch, injured.id)
);
effects.push(
createLogEffect(
`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`,
{
generalId: injured.id,
format: LogFormat.MONTH,
}
)
);
}
return { effects };
}
}
export class ActionDefinition<
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
) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
parseArgs(raw: unknown): FireAttackArgs | null {
if (!raw || typeof raw !== 'object') {
return null;
}
const destCityId = (raw as { destCityId?: unknown }).destCityId;
if (typeof destCityId !== 'number' || Number.isNaN(destCityId)) {
return null;
}
return { destCityId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: FireAttackArgs
): Constraint[] {
void _ctx;
void _args;
const { gold, rice } = this.command.getCost();
return [
notBeNeutral(),
occupiedCity(),
suppliedCity(),
existsDestCity(),
notOccupiedDestCity(),
notNeutralDestCity(),
reqGeneralGold(() => gold),
reqGeneralRice(() => rice),
disallowDiplomacyBetweenStatus({
7: '불가침국입니다.',
}),
];
}
resolve(
context: FireAttackResolveContext<TriggerState>,
args: FireAttackArgs
): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -0,0 +1,45 @@
export type GeneralTurnCommandKey = 'che_상업투자' | 'che_화계';
export type GeneralTurnCommandModule =
| typeof import('./che_상업투자.js')
| typeof import('./che_화계.js');
export type GeneralTurnCommandImporter = () => Promise<GeneralTurnCommandModule>;
const defaultImporters: Record<
GeneralTurnCommandKey,
GeneralTurnCommandImporter
> = {
che_상업투자: async () => import('./che_상업투자.js'),
che_화계: async () => import('./che_화계.js'),
};
export class GeneralTurnCommandLoader {
constructor(
private readonly importers: Record<
GeneralTurnCommandKey,
GeneralTurnCommandImporter
> = defaultImporters
) {}
async load(
key: GeneralTurnCommandKey
): Promise<GeneralTurnCommandModule> {
const importer = this.importers[key];
if (!importer) {
throw new Error(`Unknown general turn command key: ${key}`);
}
return importer();
}
}
export {
ActionDefinition as CommerceInvestmentActionDefinition,
ActionResolver as CommerceInvestmentActionResolver,
CommandResolver as CommerceInvestmentCommandResolver,
} from './che_상업투자.js';
export {
ActionDefinition as FireAttackActionDefinition,
ActionResolver as FireAttackActionResolver,
CommandResolver as FireAttackCommandResolver,
} from './che_화계.js';
@@ -0,0 +1 @@
export {};