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:
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Constraint, ConstraintContext } from '../constraints/types.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from './engine.js';
|
||||||
|
import type { GeneralTriggerState } from '../domain/entities.js';
|
||||||
|
|
||||||
|
export interface GeneralActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
Args = unknown,
|
||||||
|
Context extends GeneralActionResolveContext<TriggerState> = GeneralActionResolveContext<TriggerState>
|
||||||
|
> {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
parseArgs(raw: unknown): Args | null;
|
||||||
|
buildConstraints(ctx: ConstraintContext, args: Args): Constraint[];
|
||||||
|
resolve(context: Context, args: Args): GeneralActionOutcome<TriggerState>;
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import type { CommandResolver as CommandResolverType } from './che_상업투자.js';
|
|
||||||
|
|
||||||
export type DomesticCommandKey = 'che_상업투자';
|
|
||||||
type CommandResolverCtor = typeof CommandResolverType;
|
|
||||||
type CommandResolverArgs = ConstructorParameters<CommandResolverCtor>;
|
|
||||||
type CommandResolverInstance = InstanceType<CommandResolverCtor>;
|
|
||||||
|
|
||||||
export type DomesticCommandImporter = () => Promise<{
|
|
||||||
CommandResolver: typeof CommandResolverType;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const defaultImporters: Record<DomesticCommandKey, DomesticCommandImporter> = {
|
|
||||||
che_상업투자: async () => import('./che_상업투자.js'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DomesticCommandLoader {
|
|
||||||
constructor(
|
|
||||||
private readonly importers: Record<
|
|
||||||
DomesticCommandKey,
|
|
||||||
DomesticCommandImporter
|
|
||||||
> = defaultImporters
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async load(
|
|
||||||
key: DomesticCommandKey
|
|
||||||
): Promise<{ CommandResolver: typeof CommandResolverType }> {
|
|
||||||
const importer = this.importers[key];
|
|
||||||
if (!importer) {
|
|
||||||
throw new Error(`Unknown domestic command key: ${key}`);
|
|
||||||
}
|
|
||||||
return importer();
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
key: DomesticCommandKey,
|
|
||||||
...args: CommandResolverArgs
|
|
||||||
): Promise<CommandResolverInstance> {
|
|
||||||
const { CommandResolver } = await this.load(key);
|
|
||||||
return new CommandResolver(...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,16 +37,19 @@ export interface GeneralPatchEffect<
|
|||||||
> {
|
> {
|
||||||
type: 'general:patch';
|
type: 'general:patch';
|
||||||
patch: Partial<General<TriggerState>>;
|
patch: Partial<General<TriggerState>>;
|
||||||
|
targetId?: GeneralId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CityPatchEffect {
|
export interface CityPatchEffect {
|
||||||
type: 'city:patch';
|
type: 'city:patch';
|
||||||
patch: Partial<City>;
|
patch: Partial<City>;
|
||||||
|
targetId?: CityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NationPatchEffect {
|
export interface NationPatchEffect {
|
||||||
type: 'nation:patch';
|
type: 'nation:patch';
|
||||||
patch: Partial<Nation>;
|
patch: Partial<Nation>;
|
||||||
|
targetId?: NationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogEffect {
|
export interface LogEffect {
|
||||||
@@ -75,11 +78,13 @@ export interface GeneralActionOutcome<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GeneralActionResolver<
|
export interface GeneralActionResolver<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
Args = unknown
|
||||||
> {
|
> {
|
||||||
key: string;
|
key: string;
|
||||||
resolve(
|
resolve(
|
||||||
context: GeneralActionResolveContext<TriggerState>
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
args: Args
|
||||||
): GeneralActionOutcome<TriggerState>;
|
): GeneralActionOutcome<TriggerState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +95,11 @@ export interface GeneralActionResolution {
|
|||||||
nextTurnAt: Date;
|
nextTurnAt: Date;
|
||||||
logs: LogEntryDraft[];
|
logs: LogEntryDraft[];
|
||||||
effects: GeneralActionEffect[];
|
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?: {
|
dirty?: {
|
||||||
general: boolean;
|
general: boolean;
|
||||||
city: boolean;
|
city: boolean;
|
||||||
@@ -159,22 +169,30 @@ const applyNationPatch = (base: Nation, patch: Partial<Nation>): Nation => ({
|
|||||||
export const createGeneralPatchEffect = <
|
export const createGeneralPatchEffect = <
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
>(
|
>(
|
||||||
patch: Partial<General<TriggerState>>
|
patch: Partial<General<TriggerState>>,
|
||||||
|
targetId?: GeneralId
|
||||||
): GeneralPatchEffect<TriggerState> => ({
|
): GeneralPatchEffect<TriggerState> => ({
|
||||||
type: 'general:patch',
|
type: 'general:patch',
|
||||||
patch,
|
patch,
|
||||||
|
...(targetId !== undefined ? { targetId } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createCityPatchEffect = (patch: Partial<City>): CityPatchEffect => ({
|
export const createCityPatchEffect = (
|
||||||
|
patch: Partial<City>,
|
||||||
|
targetId?: CityId
|
||||||
|
): CityPatchEffect => ({
|
||||||
type: 'city:patch',
|
type: 'city:patch',
|
||||||
patch,
|
patch,
|
||||||
|
...(targetId !== undefined ? { targetId } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createNationPatchEffect = (
|
export const createNationPatchEffect = (
|
||||||
patch: Partial<Nation>
|
patch: Partial<Nation>,
|
||||||
|
targetId?: NationId
|
||||||
): NationPatchEffect => ({
|
): NationPatchEffect => ({
|
||||||
type: 'nation:patch',
|
type: 'nation:patch',
|
||||||
patch,
|
patch,
|
||||||
|
...(targetId !== undefined ? { targetId } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createLogEffect = (
|
export const createLogEffect = (
|
||||||
@@ -208,18 +226,25 @@ export const createNextTurnOverrideEffect = (
|
|||||||
|
|
||||||
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
|
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
|
||||||
export const resolveGeneralAction = <
|
export const resolveGeneralAction = <
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
|
Args = unknown
|
||||||
>(
|
>(
|
||||||
resolver: GeneralActionResolver<TriggerState>,
|
resolver: GeneralActionResolver<TriggerState, Args>,
|
||||||
context: GeneralActionResolveContext<TriggerState>,
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
scheduleContext: TurnScheduleContext
|
scheduleContext: TurnScheduleContext,
|
||||||
|
args: Args
|
||||||
): GeneralActionResolution => {
|
): GeneralActionResolution => {
|
||||||
const outcome = resolver.resolve(context);
|
const outcome = resolver.resolve(context, args);
|
||||||
const logs: LogEntryDraft[] = [];
|
const logs: LogEntryDraft[] = [];
|
||||||
let nextGeneral = context.general;
|
let nextGeneral = context.general;
|
||||||
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 patches: NonNullable<GeneralActionResolution['patches']> = {
|
||||||
|
generals: [],
|
||||||
|
cities: [],
|
||||||
|
nations: [],
|
||||||
|
};
|
||||||
const dirty: NonNullable<GeneralActionResolution['dirty']> = {
|
const dirty: NonNullable<GeneralActionResolution['dirty']> = {
|
||||||
general: false,
|
general: false,
|
||||||
city: false,
|
city: false,
|
||||||
@@ -236,19 +261,49 @@ export const resolveGeneralAction = <
|
|||||||
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);
|
if (
|
||||||
dirty.general = true;
|
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;
|
break;
|
||||||
case 'city:patch':
|
case 'city:patch':
|
||||||
if (nextCity) {
|
if (
|
||||||
nextCity = applyCityPatch(nextCity, effect.patch);
|
effect.targetId === undefined ||
|
||||||
dirty.city = true;
|
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;
|
break;
|
||||||
case 'nation:patch':
|
case 'nation:patch':
|
||||||
if (nextNation) {
|
if (
|
||||||
nextNation = applyNationPatch(nextNation, effect.patch);
|
effect.targetId === undefined ||
|
||||||
dirty.nation = true;
|
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;
|
break;
|
||||||
case 'log':
|
case 'log':
|
||||||
@@ -309,6 +364,13 @@ export const resolveGeneralAction = <
|
|||||||
if (dirty.general || dirty.city || dirty.nation) {
|
if (dirty.general || dirty.city || dirty.nation) {
|
||||||
resolution.dirty = dirty;
|
resolution.dirty = dirty;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
patches.generals.length > 0 ||
|
||||||
|
patches.cities.length > 0 ||
|
||||||
|
patches.nations.length > 0
|
||||||
|
) {
|
||||||
|
resolution.patches = patches;
|
||||||
|
}
|
||||||
|
|
||||||
return resolution;
|
return resolution;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
export * from './domestic/index.js';
|
export * from './definition.js';
|
||||||
export * from './engine.js';
|
export * from './engine.js';
|
||||||
|
export * from './turn/general/index.js';
|
||||||
|
export * from './turn/nation/index.js';
|
||||||
|
export * from './instant/general/index.js';
|
||||||
|
export * from './instant/nation/index.js';
|
||||||
|
export * from './admin/index.js';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
+121
-7
@@ -4,23 +4,39 @@ import type {
|
|||||||
General,
|
General,
|
||||||
GeneralTriggerState,
|
GeneralTriggerState,
|
||||||
Nation,
|
Nation,
|
||||||
} from '../../domain/entities.js';
|
} from '../../../domain/entities.js';
|
||||||
import type { GeneralActionContext } from '../../triggers/general.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 {
|
import {
|
||||||
GeneralActionPipeline,
|
GeneralActionPipeline,
|
||||||
type GeneralActionModule,
|
type GeneralActionModule,
|
||||||
} from '../../triggers/general-action.js';
|
} from '../../../triggers/general-action.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
import type {
|
import type {
|
||||||
GeneralActionOutcome,
|
GeneralActionOutcome,
|
||||||
GeneralActionResolver,
|
GeneralActionResolver,
|
||||||
GeneralActionResolveContext,
|
GeneralActionResolveContext,
|
||||||
GeneralActionEffect,
|
GeneralActionEffect,
|
||||||
} from '../engine.js';
|
} from '../../engine.js';
|
||||||
import {
|
import {
|
||||||
createCityPatchEffect,
|
createCityPatchEffect,
|
||||||
createGeneralPatchEffect,
|
createGeneralPatchEffect,
|
||||||
createLogEffect,
|
createLogEffect,
|
||||||
} from '../engine.js';
|
} from '../../engine.js';
|
||||||
|
|
||||||
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
||||||
|
|
||||||
@@ -59,6 +75,8 @@ export interface CommerceInvestmentResult {
|
|||||||
appliedFrontDebuff: boolean;
|
appliedFrontDebuff: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommerceInvestmentArgs {}
|
||||||
|
|
||||||
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];
|
||||||
@@ -108,6 +126,38 @@ const addMetaNumber = (
|
|||||||
return { ...meta, [key]: current + delta };
|
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<
|
export class CommandResolver<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -252,7 +302,7 @@ export class CommandResolver<
|
|||||||
|
|
||||||
export class ActionResolver<
|
export class ActionResolver<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> implements GeneralActionResolver<TriggerState> {
|
> implements GeneralActionResolver<TriggerState, CommerceInvestmentArgs> {
|
||||||
readonly key = 'che_상업투자';
|
readonly key = 'che_상업투자';
|
||||||
private readonly command: CommandResolver<TriggerState>;
|
private readonly command: CommandResolver<TriggerState>;
|
||||||
|
|
||||||
@@ -264,8 +314,10 @@ export class ActionResolver<
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: GeneralActionResolveContext<TriggerState>
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: CommerceInvestmentArgs
|
||||||
): GeneralActionOutcome<TriggerState> {
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
void _args;
|
||||||
const general = context.general;
|
const general = context.general;
|
||||||
const city = context.city;
|
const city = context.city;
|
||||||
if (!city) {
|
if (!city) {
|
||||||
@@ -323,3 +375,65 @@ export class ActionResolver<
|
|||||||
return { effects };
|
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 {};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
ConstraintResult,
|
||||||
|
RequirementKey,
|
||||||
|
StateView,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export const evaluateConstraints = (
|
||||||
|
constraints: Constraint[],
|
||||||
|
ctx: ConstraintContext,
|
||||||
|
view: StateView
|
||||||
|
): ConstraintResult => {
|
||||||
|
for (const constraint of constraints) {
|
||||||
|
const missing = constraint
|
||||||
|
.requires(ctx)
|
||||||
|
.filter((req) => !view.has(req));
|
||||||
|
if (missing.length > 0 && ctx.mode === 'precheck') {
|
||||||
|
return { kind: 'unknown', missing };
|
||||||
|
}
|
||||||
|
const result = constraint.test(ctx, view);
|
||||||
|
if (result.kind !== 'allow') {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { kind: 'allow' };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const collectRequirements = (
|
||||||
|
constraints: Constraint[],
|
||||||
|
ctx: ConstraintContext
|
||||||
|
): RequirementKey[] => {
|
||||||
|
const keys: RequirementKey[] = [];
|
||||||
|
for (const constraint of constraints) {
|
||||||
|
keys.push(...constraint.requires(ctx));
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './evaluate.js';
|
||||||
|
export * from './presets.js';
|
||||||
|
export * from './types.js';
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
import type { City, General, Nation } from '../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
ConstraintResult,
|
||||||
|
RequirementKey,
|
||||||
|
StateView,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
const allow = (): ConstraintResult => ({ kind: 'allow' });
|
||||||
|
|
||||||
|
const unknownOrDeny = (
|
||||||
|
ctx: ConstraintContext,
|
||||||
|
missing: RequirementKey[],
|
||||||
|
reason: string
|
||||||
|
): ConstraintResult =>
|
||||||
|
ctx.mode === 'precheck'
|
||||||
|
? { kind: 'unknown', missing }
|
||||||
|
: { kind: 'deny', reason };
|
||||||
|
|
||||||
|
const readGeneral = (
|
||||||
|
ctx: ConstraintContext,
|
||||||
|
view: StateView
|
||||||
|
): General | null => {
|
||||||
|
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return view.get(req) as General | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readCity = (view: StateView, id?: number): City | null => {
|
||||||
|
if (id === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'city', id };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return view.get(req) as City | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveDestCityId = (ctx: ConstraintContext): number | undefined => {
|
||||||
|
if (ctx.destCityId !== undefined) {
|
||||||
|
return ctx.destCityId;
|
||||||
|
}
|
||||||
|
const raw = ctx.args?.destCityId;
|
||||||
|
return typeof raw === 'number' ? raw : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveDestNationId = (ctx: ConstraintContext): number | undefined => {
|
||||||
|
if (ctx.destNationId !== undefined) {
|
||||||
|
return ctx.destNationId;
|
||||||
|
}
|
||||||
|
const raw = ctx.args?.destNationId;
|
||||||
|
return typeof raw === 'number' ? raw : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readDestCity = (
|
||||||
|
ctx: ConstraintContext,
|
||||||
|
view: StateView
|
||||||
|
): City | null => {
|
||||||
|
const destCityId = resolveDestCityId(ctx);
|
||||||
|
return readCity(view, destCityId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readNation = (
|
||||||
|
view: StateView,
|
||||||
|
id?: number
|
||||||
|
): Nation | null => {
|
||||||
|
if (id === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'nation', id };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return view.get(req) as Nation | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readDiplomacyState = (
|
||||||
|
view: StateView,
|
||||||
|
srcNationId: number,
|
||||||
|
destNationId: number
|
||||||
|
): number | null => {
|
||||||
|
const req: RequirementKey = {
|
||||||
|
kind: 'diplomacy',
|
||||||
|
srcNationId,
|
||||||
|
destNationId,
|
||||||
|
};
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const value = view.get(req);
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const state = (value as { state?: number; stateCode?: number }).state;
|
||||||
|
const stateCode = (value as { state?: number; stateCode?: number }).stateCode;
|
||||||
|
if (typeof state === 'number') {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (typeof stateCode === 'number') {
|
||||||
|
return stateCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const notBeNeutral = (): Constraint => ({
|
||||||
|
name: 'NotBeNeutral',
|
||||||
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(req) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (general.nationId !== 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '재야입니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const notWanderingNation = (): Constraint => ({
|
||||||
|
name: 'NotWanderingNation',
|
||||||
|
requires: (ctx) =>
|
||||||
|
ctx.nationId !== undefined
|
||||||
|
? [{ kind: 'nation', id: ctx.nationId }]
|
||||||
|
: [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const nation = readNation(view, ctx.nationId);
|
||||||
|
if (!nation) {
|
||||||
|
if (ctx.nationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'nation', id: ctx.nationId };
|
||||||
|
return unknownOrDeny(ctx, [req], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (nation.level !== 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '방랑군은 불가능합니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const occupiedCity = (
|
||||||
|
options: { allowNeutral?: boolean } = {}
|
||||||
|
): Constraint => ({
|
||||||
|
name: 'OccupiedCity',
|
||||||
|
requires: (ctx) => {
|
||||||
|
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||||
|
if (ctx.cityId !== undefined) {
|
||||||
|
reqs.push({ kind: 'city', id: ctx.cityId });
|
||||||
|
}
|
||||||
|
return reqs;
|
||||||
|
},
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
if (!view.has(generalReq)) {
|
||||||
|
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(generalReq) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (options.allowNeutral && general.nationId === 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
const cityId = ctx.cityId ?? general.cityId;
|
||||||
|
const cityReq: RequirementKey = { kind: 'city', id: cityId };
|
||||||
|
if (!view.has(cityReq)) {
|
||||||
|
return unknownOrDeny(ctx, [cityReq], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const city = view.get(cityReq) as City | null;
|
||||||
|
if (!city) {
|
||||||
|
return unknownOrDeny(ctx, [cityReq], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (city.nationId === general.nationId) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '아국이 아닙니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const suppliedCity = (): Constraint => ({
|
||||||
|
name: 'SuppliedCity',
|
||||||
|
requires: (ctx) =>
|
||||||
|
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const city = readCity(view, ctx.cityId);
|
||||||
|
if (!city) {
|
||||||
|
if (ctx.cityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (city.supplyState) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '고립된 도시입니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reqGeneralGold = (
|
||||||
|
getRequiredGold: (ctx: ConstraintContext, view: StateView) => number,
|
||||||
|
requirements: RequirementKey[] = []
|
||||||
|
): Constraint => ({
|
||||||
|
name: 'ReqGeneralGold',
|
||||||
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
const missing = [generalReq, ...requirements].filter(
|
||||||
|
(req) => !view.has(req)
|
||||||
|
);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(generalReq) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const required = getRequiredGold(ctx, view);
|
||||||
|
if (general.gold >= required) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '자금이 모자랍니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reqGeneralRice = (
|
||||||
|
getRequiredRice: (ctx: ConstraintContext, view: StateView) => number,
|
||||||
|
requirements: RequirementKey[] = []
|
||||||
|
): Constraint => ({
|
||||||
|
name: 'ReqGeneralRice',
|
||||||
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
const missing = [generalReq, ...requirements].filter(
|
||||||
|
(req) => !view.has(req)
|
||||||
|
);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(generalReq) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const required = getRequiredRice(ctx, view);
|
||||||
|
if (general.rice >= required) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '군량이 모자랍니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const remainCityCapacity = (
|
||||||
|
key: string,
|
||||||
|
label: string
|
||||||
|
): Constraint => ({
|
||||||
|
name: 'RemainCityCapacity',
|
||||||
|
requires: (ctx) =>
|
||||||
|
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const city = readCity(view, ctx.cityId);
|
||||||
|
if (!city) {
|
||||||
|
if (ctx.cityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const record = city as unknown as Record<string, number | undefined>;
|
||||||
|
const maxKey = `${key}_max`;
|
||||||
|
const current = record[key];
|
||||||
|
const max = record[maxKey];
|
||||||
|
if (current === undefined || max === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (current < max) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: `${label}이 충분합니다.` };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const existsDestCity = (): Constraint => ({
|
||||||
|
name: 'ExistsDestCity',
|
||||||
|
requires: (ctx) =>
|
||||||
|
resolveDestCityId(ctx) !== undefined
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
kind: 'destCity',
|
||||||
|
id: resolveDestCityId(ctx) ?? 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const destCityId = resolveDestCityId(ctx);
|
||||||
|
if (destCityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'destCity', id: destCityId };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const city = view.get(req) as City | null;
|
||||||
|
if (!city) {
|
||||||
|
return { kind: 'deny', reason: '도시 정보가 없습니다.' };
|
||||||
|
}
|
||||||
|
return allow();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const notOccupiedDestCity = (): Constraint => ({
|
||||||
|
name: 'NotOccupiedDestCity',
|
||||||
|
requires: (ctx) => {
|
||||||
|
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||||
|
const destCityId = resolveDestCityId(ctx);
|
||||||
|
if (destCityId !== undefined) {
|
||||||
|
reqs.push({ kind: 'destCity', id: destCityId });
|
||||||
|
}
|
||||||
|
return reqs;
|
||||||
|
},
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
if (!view.has(generalReq)) {
|
||||||
|
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(generalReq) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const destCity = readDestCity(ctx, view);
|
||||||
|
if (!destCity) {
|
||||||
|
const destCityId = resolveDestCityId(ctx);
|
||||||
|
if (destCityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = {
|
||||||
|
kind: 'destCity',
|
||||||
|
id: destCityId,
|
||||||
|
};
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (destCity.nationId !== general.nationId) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '아국입니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const notNeutralDestCity = (): Constraint => ({
|
||||||
|
name: 'NotNeutralDestCity',
|
||||||
|
requires: (ctx) =>
|
||||||
|
resolveDestCityId(ctx) !== undefined
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
kind: 'destCity',
|
||||||
|
id: resolveDestCityId(ctx) ?? 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const destCity = readDestCity(ctx, view);
|
||||||
|
if (!destCity) {
|
||||||
|
const destCityId = resolveDestCityId(ctx);
|
||||||
|
if (destCityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = {
|
||||||
|
kind: 'destCity',
|
||||||
|
id: destCityId,
|
||||||
|
};
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (destCity.nationId !== 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '공백지입니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const disallowDiplomacyBetweenStatus = (
|
||||||
|
disallowList: Record<number, string>
|
||||||
|
): Constraint => ({
|
||||||
|
name: 'DisallowDiplomacyBetweenStatus',
|
||||||
|
requires: (ctx) => {
|
||||||
|
const reqs: RequirementKey[] = [];
|
||||||
|
if (ctx.nationId !== undefined) {
|
||||||
|
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||||
|
}
|
||||||
|
const destNationId = resolveDestNationId(ctx);
|
||||||
|
if (destNationId !== undefined) {
|
||||||
|
reqs.push({ kind: 'destNation', id: destNationId });
|
||||||
|
if (ctx.nationId !== undefined) {
|
||||||
|
reqs.push({
|
||||||
|
kind: 'diplomacy',
|
||||||
|
srcNationId: ctx.nationId,
|
||||||
|
destNationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const destCityId = resolveDestCityId(ctx);
|
||||||
|
if (destCityId !== undefined) {
|
||||||
|
reqs.push({ kind: 'destCity', id: destCityId });
|
||||||
|
}
|
||||||
|
return reqs;
|
||||||
|
},
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const general = readGeneral(ctx, view);
|
||||||
|
const baseNationId = ctx.nationId ?? general?.nationId;
|
||||||
|
if (baseNationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const destCity = readDestCity(ctx, view);
|
||||||
|
const destNationId =
|
||||||
|
resolveDestNationId(ctx) ?? destCity?.nationId;
|
||||||
|
if (destNationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const state = readDiplomacyState(view, baseNationId, destNationId);
|
||||||
|
if (state === null) {
|
||||||
|
const req: RequirementKey = {
|
||||||
|
kind: 'diplomacy',
|
||||||
|
srcNationId: baseNationId,
|
||||||
|
destNationId,
|
||||||
|
};
|
||||||
|
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const reason = disallowList[state];
|
||||||
|
if (reason !== undefined) {
|
||||||
|
return { kind: 'deny', reason };
|
||||||
|
}
|
||||||
|
return allow();
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export type ConstraintResult =
|
||||||
|
| { kind: 'allow' }
|
||||||
|
| { kind: 'deny'; reason: string; code?: string }
|
||||||
|
| { kind: 'unknown'; missing: RequirementKey[] };
|
||||||
|
|
||||||
|
export type RequirementKey =
|
||||||
|
| { kind: 'general'; id: number }
|
||||||
|
| { kind: 'city'; id: number }
|
||||||
|
| { kind: 'nation'; id: number }
|
||||||
|
| { kind: 'destGeneral'; id: number }
|
||||||
|
| { kind: 'destCity'; id: number }
|
||||||
|
| { kind: 'destNation'; id: number }
|
||||||
|
| { kind: 'diplomacy'; srcNationId: number; destNationId: number }
|
||||||
|
| { kind: 'arg'; key: string }
|
||||||
|
| { kind: 'env'; key: string };
|
||||||
|
|
||||||
|
export interface ConstraintContext {
|
||||||
|
actorId: number;
|
||||||
|
cityId?: number;
|
||||||
|
nationId?: number;
|
||||||
|
destGeneralId?: number;
|
||||||
|
destCityId?: number;
|
||||||
|
destNationId?: number;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
env: Record<string, unknown>;
|
||||||
|
mode: 'full' | 'precheck';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateView {
|
||||||
|
has(req: RequirementKey): boolean;
|
||||||
|
get(req: RequirementKey): unknown | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Constraint {
|
||||||
|
name: string;
|
||||||
|
requires(ctx: ConstraintContext): RequirementKey[];
|
||||||
|
test(ctx: ConstraintContext, view: StateView): ConstraintResult;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './domain/entities.js';
|
export * from './domain/entities.js';
|
||||||
export type { RandomGenerator } from '@sammo-ts/common';
|
export type { RandomGenerator } from '@sammo-ts/common';
|
||||||
export * from './actions/index.js';
|
export * from './actions/index.js';
|
||||||
|
export * from './constraints/index.js';
|
||||||
export * from './logging/index.js';
|
export * from './logging/index.js';
|
||||||
export * from './ports/world.js';
|
export * from './ports/world.js';
|
||||||
export * from './ports/worldSnapshot.js';
|
export * from './ports/worldSnapshot.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user