feat: Introduce action context builders for various turn actions

- Added `actionContextBuilder` to multiple turn action files to utilize the default action context builder.
- Implemented specific context builders for actions requiring additional context, such as war configurations, world summaries, and general statistics.
- Created new `actionContext.ts` and `actionContextHelpers.ts` files to define interfaces and helper functions for managing action contexts.
- Enhanced context handling for nation and general actions, ensuring proper data injection for turn execution.
This commit is contained in:
2026-01-03 06:28:50 +00:00
parent b628b32968
commit 4f90f50033
31 changed files with 613 additions and 429 deletions
@@ -1,437 +1,19 @@
import type {
City,
General,
MapDefinition,
Nation,
ScenarioConfig,
ScenarioMeta,
WarAftermathConfig,
WarEngineConfig,
WarTimeContext,
UnitSetDefinition,
ActionContextBase,
ActionContextBuilder,
ActionContextOptions,
ActionResolveContext,
} from '@sammo-ts/logic';
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
import type { TurnGeneral, TurnWorldState } from './types.js';
interface WorldSummary {
totalGeneralCount: number;
totalNpcCount: number;
averageStats?: General['stats'];
}
interface NationSummary {
averageStats?: General['stats'];
averageExperience?: number;
averageDedication?: number;
}
export interface ActionRandomSource {
nextFloat(): number;
nextBool(probability: number): boolean;
nextInt(minInclusive: number, maxExclusive: number): number;
}
export type ActionContextBase = {
general: TurnGeneral;
city?: City;
nation?: Nation | null;
rng: ActionRandomSource;
};
export type ActionResolveContext = ActionContextBase & Record<string, unknown>;
export interface ActionContextOptions {
world: TurnWorldState;
scenarioConfig: ScenarioConfig;
scenarioMeta?: ScenarioMeta;
map?: MapDefinition;
unitSet?: UnitSetDefinition;
worldRef: InMemoryTurnWorld | null;
actionArgs: Record<string, unknown>;
createGeneralId: () => number;
seedBase: string;
}
type ActionContextBuilder = (
base: ActionContextBase,
options: ActionContextOptions
) => ActionResolveContext | null;
const buildWorldSummary = (world: InMemoryTurnWorld | null): WorldSummary => {
if (!world) {
return { totalGeneralCount: 0, totalNpcCount: 0 };
}
const generals = world.listGenerals();
if (generals.length === 0) {
return { totalGeneralCount: 0, totalNpcCount: 0 };
}
const total = generals.length;
const npcCount = generals.filter((general) => general.npcState > 0).length;
const statSum = generals.reduce(
(acc, general) => ({
leadership: acc.leadership + general.stats.leadership,
strength: acc.strength + general.stats.strength,
intelligence: acc.intelligence + general.stats.intelligence,
}),
{ leadership: 0, strength: 0, intelligence: 0 }
);
return {
totalGeneralCount: total,
totalNpcCount: npcCount,
averageStats: {
leadership: statSum.leadership / total,
strength: statSum.strength / total,
intelligence: statSum.intelligence / total,
},
};
};
const buildNationSummary = (
world: InMemoryTurnWorld | null,
nationId: number
): NationSummary => {
if (!world || nationId <= 0) {
return {};
}
const generals = world.listGenerals().filter(
(general) => general.nationId === nationId
);
if (generals.length === 0) {
return {};
}
const total = generals.length;
const statSum = generals.reduce(
(acc, general) => ({
leadership: acc.leadership + general.stats.leadership,
strength: acc.strength + general.stats.strength,
intelligence: acc.intelligence + general.stats.intelligence,
}),
{ leadership: 0, strength: 0, intelligence: 0 }
);
const expSum = generals.reduce((acc, general) => acc + general.experience, 0);
const dedSum = generals.reduce((acc, general) => acc + general.dedication, 0);
return {
averageStats: {
leadership: statSum.leadership / total,
strength: statSum.strength / total,
intelligence: statSum.intelligence / total,
},
averageExperience: expSum / total,
averageDedication: dedSum / total,
};
};
const buildAverageNationGeneralCount = (world: InMemoryTurnWorld | null): number => {
if (!world) {
return 0;
}
const generals = world.listGenerals();
const nations = world.listNations();
if (nations.length === 0) {
return generals.length;
}
return generals.length / nations.length;
};
const resolveStartYear = (
world: TurnWorldState,
scenarioMeta?: ScenarioMeta
): number => {
if (typeof scenarioMeta?.startYear === 'number') {
return scenarioMeta.startYear;
}
return world.currentYear;
};
const resolveTurnTermMinutes = (world: TurnWorldState): number =>
Math.max(1, Math.round(world.tickSeconds / 60));
const DEFAULT_WAR_CONFIG = {
armPerPhase: 500,
maxTrainByCommand: 100,
maxAtmosByCommand: 100,
maxTrainByWar: 110,
maxAtmosByWar: 150,
};
const DEFAULT_AFTER_CONFIG = {
techLevelIncYear: 5,
initialAllowedTechLevel: 1,
defaultCityWall: 1000,
};
const asRecord = (value: unknown): Record<string, unknown> =>
value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const resolveNumber = (
record: Record<string, unknown>,
keys: string[],
fallback: number
): number => {
for (const key of keys) {
const value = record[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
}
return fallback;
};
const resolveCastleCrewTypeId = (
unitSet: UnitSetDefinition,
fallback: number
): number => {
const crewTypes = unitSet.crewTypes ?? [];
const byName = crewTypes.find((crewType) => crewType.name.includes('성벽'));
if (byName) {
return byName.id;
}
const byRequirement = crewTypes.find((crewType) =>
crewType.requirements.some(
(requirement) => requirement.type === 'Impossible'
)
);
if (byRequirement) {
return byRequirement.id;
}
if (typeof unitSet.defaultCrewTypeId === 'number') {
return unitSet.defaultCrewTypeId;
}
return crewTypes[0]?.id ?? fallback;
};
const resolveCastleArmType = (
unitSet: UnitSetDefinition,
castleCrewTypeId: number
): number => {
const crewTypes = unitSet.crewTypes ?? [];
return (
crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ??
0
);
};
const buildWarConfig = (
scenarioConfig: ScenarioConfig,
unitSet: UnitSetDefinition
): WarEngineConfig => {
const constValues = asRecord(scenarioConfig.const);
const castleCrewTypeId = resolveNumber(
constValues,
['castleCrewTypeId'],
resolveCastleCrewTypeId(unitSet, 0)
);
const castleArmType = resolveCastleArmType(unitSet, castleCrewTypeId);
return {
armPerPhase: resolveNumber(
constValues,
['armPerPhase', 'armperphase'],
DEFAULT_WAR_CONFIG.armPerPhase
),
maxTrainByCommand: resolveNumber(
constValues,
['maxTrainByCommand'],
DEFAULT_WAR_CONFIG.maxTrainByCommand
),
maxAtmosByCommand: resolveNumber(
constValues,
['maxAtmosByCommand'],
DEFAULT_WAR_CONFIG.maxAtmosByCommand
),
maxTrainByWar: resolveNumber(
constValues,
['maxTrainByWar'],
DEFAULT_WAR_CONFIG.maxTrainByWar
),
maxAtmosByWar: resolveNumber(
constValues,
['maxAtmosByWar'],
DEFAULT_WAR_CONFIG.maxAtmosByWar
),
castleCrewTypeId,
armTypes: {
footman: 1,
archer: 2,
cavalry: 3,
wizard: 4,
siege: 5,
misc: 6,
castle: castleArmType,
},
};
};
const buildWarAftermathConfig = (
scenarioConfig: ScenarioConfig,
castleCrewTypeId: number
): WarAftermathConfig => {
const constValues = asRecord(scenarioConfig.const);
return {
initialNationGenLimit: resolveNumber(
constValues,
['initialNationGenLimit'],
0
),
techLevelIncYear: resolveNumber(
constValues,
['techLevelIncYear'],
DEFAULT_AFTER_CONFIG.techLevelIncYear
),
initialAllowedTechLevel: resolveNumber(
constValues,
['initialAllowedTechLevel'],
DEFAULT_AFTER_CONFIG.initialAllowedTechLevel
),
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
defaultCityWall: resolveNumber(
constValues,
['defaultCityWall'],
DEFAULT_AFTER_CONFIG.defaultCityWall
),
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
castleCrewTypeId,
};
};
const buildWarTime = (
world: TurnWorldState,
scenarioMeta?: ScenarioMeta
): WarTimeContext => ({
year: world.currentYear,
month: world.currentMonth,
startYear: resolveStartYear(world, scenarioMeta),
});
// 커맨드별로 필요한 컨텍스트 확장 데이터를 구성한다.
const ACTION_CONTEXT_BUILDERS: Record<string, ActionContextBuilder> = {
che_인재탐색: (base, options) => ({
...base,
currentYear: options.world.currentYear,
worldSummary: buildWorldSummary(options.worldRef),
createGeneralId: options.createGeneralId,
}),
che_의병모집: (base, options) => {
const nationSummary = buildNationSummary(
options.worldRef,
base.general.nationId
);
return {
...base,
currentYear: options.world.currentYear,
startYear: resolveStartYear(options.world, options.scenarioMeta),
averageNationGeneralCount: buildAverageNationGeneralCount(
options.worldRef
),
nationAverageStats: nationSummary.averageStats,
nationAverageExperience: nationSummary.averageExperience,
nationAverageDedication: nationSummary.averageDedication,
createGeneralId: options.createGeneralId,
};
},
che_포상: (base, options) => {
const destGeneralId = options.actionArgs.destGeneralId;
if (typeof destGeneralId !== 'number') {
return null;
}
const destGeneral = options.worldRef?.getGeneralById(destGeneralId);
if (!destGeneral) {
return null;
}
return {
...base,
destGeneral,
};
},
che_발령: (base, options) => {
const destGeneralId = options.actionArgs.destGeneralId;
const destCityId = options.actionArgs.destCityId;
if (typeof destGeneralId !== 'number' || typeof destCityId !== 'number') {
return null;
}
const destGeneral = options.worldRef?.getGeneralById(destGeneralId);
const destCity = options.worldRef?.getCityById(destCityId);
if (!destGeneral || !destCity) {
return null;
}
return {
...base,
destGeneral,
destCity,
currentYear: options.world.currentYear,
currentMonth: options.world.currentMonth,
turnTermMinutes: resolveTurnTermMinutes(options.world),
generalTurnTime: base.general.turnTime,
destGeneralTurnTime: destGeneral.turnTime,
};
},
che_징병: (base, options) => {
if (!options.map || !options.unitSet) {
return null;
}
return {
...base,
map: options.map,
unitSet: options.unitSet,
cities: options.worldRef?.listCities() ?? [],
currentYear: options.world.currentYear,
startYear: resolveStartYear(options.world, options.scenarioMeta),
};
},
che_불가침제의: (base, options) => ({
...base,
currentYear: options.world.currentYear,
currentMonth: options.world.currentMonth,
}),
che_출병: (base, options) => {
if (!options.unitSet || !options.worldRef) {
return null;
}
const destCityId = options.actionArgs.destCityId;
if (typeof destCityId !== 'number') {
return null;
}
const destCity = options.worldRef.getCityById(destCityId);
if (!destCity) {
return null;
}
const destNation =
destCity.nationId > 0
? options.worldRef.getNationById(destCity.nationId)
: null;
const diplomacy = options.worldRef.listDiplomacy();
const warConfig = buildWarConfig(options.scenarioConfig, options.unitSet);
const aftermathConfig = buildWarAftermathConfig(
options.scenarioConfig,
warConfig.castleCrewTypeId
);
return {
...base,
destCity,
destNation,
cities: options.worldRef.listCities(),
nations: options.worldRef.listNations(),
generals: options.worldRef.listGenerals(),
unitSet: options.unitSet,
map: options.map,
diplomacy,
time: buildWarTime(options.world, options.scenarioMeta),
seedBase: options.seedBase,
warConfig,
aftermathConfig,
};
},
};
export type ActionContextBuilderMap = Map<string, ActionContextBuilder>;
// 커맨드 모듈에서 제공한 컨텍스트 빌더로 확장한다.
export const buildActionContext = (
key: string,
base: ActionContextBase,
options: ActionContextOptions
options: ActionContextOptions,
builders?: ActionContextBuilderMap
): ActionResolveContext | null => {
const builder = ACTION_CONTEXT_BUILDERS[key];
const builder = builders?.get(key);
return builder ? builder(base, options) : base;
};
@@ -1,4 +1,6 @@
import type {
ActionContextBase,
ActionContextBuilder,
City,
GeneralActionDefinition,
LogEntryDraft,
@@ -12,6 +14,9 @@ import type {
} from '@sammo-ts/logic';
import {
DEFAULT_TURN_COMMAND_PROFILE,
GeneralTurnCommandLoader,
NationTurnCommandLoader,
defaultActionContextBuilder,
evaluateConstraints,
resolveGeneralAction,
} from '@sammo-ts/logic';
@@ -29,7 +34,6 @@ import {
buildCommandEnv,
buildReservedTurnDefinitions,
} from './reservedTurnCommands.js';
import type { ActionContextBase } from './reservedTurnActionContext.js';
import { buildActionContext } from './reservedTurnActionContext.js';
const DEFAULT_ACTION = '휴식';
@@ -219,6 +223,41 @@ export const createReservedTurnHandler = async (options: {
const generalFallback = generalDefinitions.get(DEFAULT_ACTION)!;
const nationFallback = nationDefinitions.get(DEFAULT_ACTION)!;
const actionContextBuilders = new Map<string, ActionContextBuilder>();
const seenActionKeys = new Set<string>();
const applyActionContextBuilder = (module: {
commandSpec: { key: string };
actionContextBuilder?: ActionContextBuilder;
}): void => {
actionContextBuilders.set(
module.commandSpec.key,
module.actionContextBuilder ?? defaultActionContextBuilder
);
};
const generalModuleLoader = new GeneralTurnCommandLoader();
const nationModuleLoader = new NationTurnCommandLoader();
for (const key of commandProfile.general) {
if (seenActionKeys.has(key)) {
continue;
}
seenActionKeys.add(key);
const module = await generalModuleLoader.load(key);
applyActionContextBuilder(module);
}
for (const key of commandProfile.nation) {
if (seenActionKeys.has(key)) {
continue;
}
seenActionKeys.add(key);
const module = await nationModuleLoader.load(key);
applyActionContextBuilder(module);
}
if (!actionContextBuilders.has(DEFAULT_ACTION)) {
applyActionContextBuilder(
await generalModuleLoader.load(DEFAULT_ACTION)
);
}
let nextGeneralId: number | null = null;
const createGeneralId = (): number => {
if (nextGeneralId === null) {
@@ -344,7 +383,7 @@ export const createReservedTurnHandler = async (options: {
actionArgs: actionArgsRecord,
createGeneralId,
seedBase,
});
}, actionContextBuilders);
if (!specificContext && actionKey !== fallbackDefinition.key) {
definition = fallbackDefinition;
actionArgs = definition.parseArgs({}) ?? {};
+1
View File
@@ -1,6 +1,7 @@
export * from './definition.js';
export * from './engine.js';
export * from './turn/commandEnv.js';
export * from './turn/actionContext.js';
export * from './turn/commandModule.js';
export * from './turn/commandProfile.js';
export * from './turn/general/index.js';
@@ -0,0 +1,63 @@
import type { City, General, Nation } from '../../domain/entities.js';
import type { ScenarioConfig } from '../../scenario/types.js';
import type { ScenarioMeta } from '../../world/types.js';
import type { MapDefinition, UnitSetDefinition } from '../../world/types.js';
export interface ActionRandomSource {
nextFloat(): number;
nextBool(probability: number): boolean;
nextInt(minInclusive: number, maxExclusive: number): number;
}
export interface ActionContextGeneral extends General {
turnTime: Date;
}
export type ActionContextBase = {
general: ActionContextGeneral;
city?: City;
nation?: Nation | null;
rng: ActionRandomSource;
};
export type ActionResolveContext = ActionContextBase & Record<string, unknown>;
export interface ActionContextWorldState {
currentYear: number;
currentMonth: number;
tickSeconds: number;
}
export interface ActionContextWorldRef {
listGenerals(): ActionContextGeneral[];
listCities(): City[];
listNations(): Nation[];
listDiplomacy(): Array<{
fromNationId: number;
toNationId: number;
state: number;
}>;
getGeneralById(id: number): ActionContextGeneral | null;
getCityById(id: number): City | null;
getNationById(id: number): Nation | null;
}
export interface ActionContextOptions {
world: ActionContextWorldState;
scenarioConfig: ScenarioConfig;
scenarioMeta?: ScenarioMeta;
map?: MapDefinition;
unitSet?: UnitSetDefinition;
worldRef: ActionContextWorldRef | null;
actionArgs: Record<string, unknown>;
createGeneralId: () => number;
seedBase: string;
}
// 예약 턴 처리에서 커맨드별로 필요한 컨텍스트를 확장한다.
export type ActionContextBuilder = (
base: ActionContextBase,
options: ActionContextOptions
) => ActionResolveContext | null;
export const defaultActionContextBuilder: ActionContextBuilder = (base) => base;
@@ -0,0 +1,275 @@
import type { General } from '../../domain/entities.js';
import type { ScenarioConfig } from '../../scenario/types.js';
import type { ScenarioMeta } from '../../world/types.js';
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '../../war/types.js';
import type { UnitSetDefinition } from '../../world/types.js';
import type {
ActionContextWorldRef,
ActionContextWorldState,
} from './actionContext.js';
export interface WorldSummary {
totalGeneralCount: number;
totalNpcCount: number;
averageStats?: General['stats'];
}
export interface NationSummary {
averageStats?: General['stats'];
averageExperience?: number;
averageDedication?: number;
}
export const buildWorldSummary = (
world: ActionContextWorldRef | null
): WorldSummary => {
if (!world) {
return { totalGeneralCount: 0, totalNpcCount: 0 };
}
const generals = world.listGenerals();
if (generals.length === 0) {
return { totalGeneralCount: 0, totalNpcCount: 0 };
}
const total = generals.length;
const npcCount = generals.filter((general) => general.npcState > 0).length;
const statSum = generals.reduce(
(acc, general) => ({
leadership: acc.leadership + general.stats.leadership,
strength: acc.strength + general.stats.strength,
intelligence: acc.intelligence + general.stats.intelligence,
}),
{ leadership: 0, strength: 0, intelligence: 0 }
);
return {
totalGeneralCount: total,
totalNpcCount: npcCount,
averageStats: {
leadership: statSum.leadership / total,
strength: statSum.strength / total,
intelligence: statSum.intelligence / total,
},
};
};
export const buildNationSummary = (
world: ActionContextWorldRef | null,
nationId: number
): NationSummary => {
if (!world || nationId <= 0) {
return {};
}
const generals = world.listGenerals().filter(
(general) => general.nationId === nationId
);
if (generals.length === 0) {
return {};
}
const total = generals.length;
const statSum = generals.reduce(
(acc, general) => ({
leadership: acc.leadership + general.stats.leadership,
strength: acc.strength + general.stats.strength,
intelligence: acc.intelligence + general.stats.intelligence,
}),
{ leadership: 0, strength: 0, intelligence: 0 }
);
const expSum = generals.reduce((acc, general) => acc + general.experience, 0);
const dedSum = generals.reduce((acc, general) => acc + general.dedication, 0);
return {
averageStats: {
leadership: statSum.leadership / total,
strength: statSum.strength / total,
intelligence: statSum.intelligence / total,
},
averageExperience: expSum / total,
averageDedication: dedSum / total,
};
};
export const buildAverageNationGeneralCount = (
world: ActionContextWorldRef | null
): number => {
if (!world) {
return 0;
}
const generals = world.listGenerals();
const nations = world.listNations();
if (nations.length === 0) {
return generals.length;
}
return generals.length / nations.length;
};
export const resolveStartYear = (
world: ActionContextWorldState,
scenarioMeta?: ScenarioMeta
): number => {
if (typeof scenarioMeta?.startYear === 'number') {
return scenarioMeta.startYear;
}
return world.currentYear;
};
export const resolveTurnTermMinutes = (world: ActionContextWorldState): number =>
Math.max(1, Math.round(world.tickSeconds / 60));
const DEFAULT_WAR_CONFIG = {
armPerPhase: 500,
maxTrainByCommand: 100,
maxAtmosByCommand: 100,
maxTrainByWar: 110,
maxAtmosByWar: 150,
};
const DEFAULT_AFTER_CONFIG = {
techLevelIncYear: 5,
initialAllowedTechLevel: 1,
defaultCityWall: 1000,
};
const asRecord = (value: unknown): Record<string, unknown> =>
value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const resolveNumber = (
record: Record<string, unknown>,
keys: string[],
fallback: number
): number => {
for (const key of keys) {
const value = record[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
}
return fallback;
};
// 성벽 병종은 이름/요구조건을 우선해 찾고, 없으면 기본값을 사용한다.
const resolveCastleCrewTypeId = (
unitSet: UnitSetDefinition,
fallback: number
): number => {
const crewTypes = unitSet.crewTypes ?? [];
const byName = crewTypes.find((crewType) => crewType.name.includes('성벽'));
if (byName) {
return byName.id;
}
const byRequirement = crewTypes.find((crewType) =>
crewType.requirements.some(
(requirement) => requirement.type === 'Impossible'
)
);
if (byRequirement) {
return byRequirement.id;
}
if (typeof unitSet.defaultCrewTypeId === 'number') {
return unitSet.defaultCrewTypeId;
}
return crewTypes[0]?.id ?? fallback;
};
const resolveCastleArmType = (
unitSet: UnitSetDefinition,
castleCrewTypeId: number
): number => {
const crewTypes = unitSet.crewTypes ?? [];
return (
crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ??
0
);
};
export const buildWarConfig = (
scenarioConfig: ScenarioConfig,
unitSet: UnitSetDefinition
): WarEngineConfig => {
const constValues = asRecord(scenarioConfig.const);
const castleCrewTypeId = resolveNumber(
constValues,
['castleCrewTypeId'],
resolveCastleCrewTypeId(unitSet, 0)
);
const castleArmType = resolveCastleArmType(unitSet, castleCrewTypeId);
return {
armPerPhase: resolveNumber(
constValues,
['armPerPhase', 'armperphase'],
DEFAULT_WAR_CONFIG.armPerPhase
),
maxTrainByCommand: resolveNumber(
constValues,
['maxTrainByCommand'],
DEFAULT_WAR_CONFIG.maxTrainByCommand
),
maxAtmosByCommand: resolveNumber(
constValues,
['maxAtmosByCommand'],
DEFAULT_WAR_CONFIG.maxAtmosByCommand
),
maxTrainByWar: resolveNumber(
constValues,
['maxTrainByWar'],
DEFAULT_WAR_CONFIG.maxTrainByWar
),
maxAtmosByWar: resolveNumber(
constValues,
['maxAtmosByWar'],
DEFAULT_WAR_CONFIG.maxAtmosByWar
),
castleCrewTypeId,
armTypes: {
footman: 1,
archer: 2,
cavalry: 3,
wizard: 4,
siege: 5,
misc: 6,
castle: castleArmType,
},
};
};
export const buildWarAftermathConfig = (
scenarioConfig: ScenarioConfig,
castleCrewTypeId: number
): WarAftermathConfig => {
const constValues = asRecord(scenarioConfig.const);
return {
initialNationGenLimit: resolveNumber(
constValues,
['initialNationGenLimit'],
0
),
techLevelIncYear: resolveNumber(
constValues,
['techLevelIncYear'],
DEFAULT_AFTER_CONFIG.techLevelIncYear
),
initialAllowedTechLevel: resolveNumber(
constValues,
['initialAllowedTechLevel'],
DEFAULT_AFTER_CONFIG.initialAllowedTechLevel
),
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
defaultCityWall: resolveNumber(
constValues,
['defaultCityWall'],
DEFAULT_AFTER_CONFIG.defaultCityWall
),
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
castleCrewTypeId,
};
};
export const buildWarTime = (
world: ActionContextWorldState,
scenarioMeta?: ScenarioMeta
): WarTimeContext => ({
year: world.currentYear,
month: world.currentMonth,
startYear: resolveStartYear(world, scenarioMeta),
});
@@ -1,5 +1,6 @@
import type { GeneralActionDefinition } from '../definition.js';
import type { GeneralActionResolver } from '../engine.js';
import type { ActionContextBuilder } from './actionContext.js';
import type { TurnCommandEnv } from './commandEnv.js';
export interface TurnCommandSpecBase<TKey extends string = string> {
@@ -15,4 +16,5 @@ export interface TurnCommandModule<TSpec extends TurnCommandSpecBase = TurnComma
ActionDefinition: new (...args: any[]) => GeneralActionDefinition;
ActionResolver?: new (...args: any[]) => GeneralActionResolver;
CommandResolver?: new (...args: any[]) => unknown;
actionContextBuilder?: ActionContextBuilder;
}
@@ -8,6 +8,7 @@ import type {
} from '../../engine.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface UprisingArgs {}
@@ -50,6 +51,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_거병',
category: '전략',
@@ -8,6 +8,7 @@ import type {
} from '../../engine.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface FoundingArgs {}
@@ -50,6 +51,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_건국',
category: '전략',
@@ -20,6 +20,7 @@ import type {
GeneralActionResolveContext,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface TechResearchArgs {}
@@ -104,6 +105,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_기술연구',
category: '내정',
@@ -1,6 +1,7 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
@@ -21,6 +22,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_농지개간',
category: '내정',
@@ -13,6 +13,7 @@ import type {
GeneralActionResolveContext,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface BoostMoraleArgs {}
@@ -82,6 +83,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_사기진작',
category: '군사',
@@ -32,6 +32,7 @@ import type {
GeneralActionResolveContext,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
@@ -422,6 +423,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_상업투자',
category: '내정',
@@ -1,6 +1,7 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
@@ -21,6 +22,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_성벽보수',
category: '내정',
@@ -1,6 +1,7 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
@@ -21,6 +22,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_수비강화',
category: '내정',
@@ -13,6 +13,7 @@ import type {
GeneralActionResolveContext,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface RecoveryArgs {}
@@ -70,6 +71,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_요양',
category: '개인',
@@ -30,6 +30,8 @@ import {
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { buildRecruitmentGeneral } from './recruitment.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBuilder } from '../actionContext.js';
import { buildWorldSummary } from '../actionContextHelpers.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -458,6 +460,14 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 월드 요약/생성기를 주입한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
...base,
currentYear: options.world.currentYear,
worldSummary: buildWorldSummary(options.worldRef),
createGeneralId: options.createGeneralId,
});
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_인재탐색',
category: '인사',
@@ -11,6 +11,7 @@ import type {
} from '../../engine.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface AppointmentArgs {
@@ -60,6 +61,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_임관',
category: '전략',
@@ -1,6 +1,7 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
@@ -21,6 +22,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_주민선정',
category: '내정',
@@ -31,6 +31,8 @@ import type {
GeneralActionResolver,
} from '../../engine.js';
import type { MapDefinition, UnitSetDefinition } from '../../../world/types.js';
import type { ActionContextBuilder } from '../actionContext.js';
import { resolveStartYear } from '../actionContextHelpers.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import {
@@ -592,6 +594,21 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 지도/연도 컨텍스트를 구성한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
if (!options.map || !options.unitSet) {
return null;
}
return {
...base,
map: options.map,
unitSet: options.unitSet,
cities: options.worldRef?.listCities() ?? [],
currentYear: options.world.currentYear,
startYear: resolveStartYear(options.world, options.scenarioMeta),
};
};
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_징병',
category: '내정',
@@ -52,6 +52,12 @@ import type {
MapDefinition,
UnitSetDefinition,
} from '../../../world/types.js';
import type { ActionContextBuilder } from '../actionContext.js';
import {
buildWarAftermathConfig,
buildWarConfig,
buildWarTime,
} from '../actionContextHelpers.js';
export interface DispatchArgs {
destCityId: number;
@@ -529,6 +535,46 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 전투 컨텍스트를 구성한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
if (!options.unitSet || !options.worldRef) {
return null;
}
const destCityId = options.actionArgs.destCityId;
if (typeof destCityId !== 'number') {
return null;
}
const destCity = options.worldRef.getCityById(destCityId);
if (!destCity) {
return null;
}
const destNation =
destCity.nationId > 0
? options.worldRef.getNationById(destCity.nationId)
: null;
const diplomacy = options.worldRef.listDiplomacy();
const warConfig = buildWarConfig(options.scenarioConfig, options.unitSet);
const aftermathConfig = buildWarAftermathConfig(
options.scenarioConfig,
warConfig.castleCrewTypeId
);
return {
...base,
destCity,
destNation,
cities: options.worldRef.listCities(),
nations: options.worldRef.listNations(),
generals: options.worldRef.listGenerals(),
unitSet: options.unitSet,
map: options.map,
diplomacy,
time: buildWarTime(options.world, options.scenarioMeta),
seedBase: options.seedBase,
warConfig,
aftermathConfig,
};
};
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_출병',
category: '군사',
@@ -1,6 +1,7 @@
import type { GeneralTriggerState } from '../../../domain/entities.js';
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
@@ -21,6 +22,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_치안강화',
category: '내정',
@@ -36,6 +36,7 @@ import {
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface FireAttackArgs {
@@ -495,6 +496,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_화계',
category: '계략',
@@ -13,6 +13,7 @@ import type {
GeneralActionResolveContext,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface TrainingArgs {}
@@ -82,6 +83,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_훈련',
category: '군사',
@@ -13,6 +13,7 @@ import type {
} from '../../engine.js';
import { LogCategory, LogFormat } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
export interface RestArgs {}
@@ -65,6 +66,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: '휴식',
category: '개인',
@@ -29,6 +29,8 @@ import type {
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBuilder } from '../actionContext.js';
import { resolveTurnTermMinutes } from '../actionContextHelpers.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
@@ -211,6 +213,30 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 대상 장수/도시 컨텍스트를 구성한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const destGeneralId = options.actionArgs.destGeneralId;
const destCityId = options.actionArgs.destCityId;
if (typeof destGeneralId !== 'number' || typeof destCityId !== 'number') {
return null;
}
const destGeneral = options.worldRef?.getGeneralById(destGeneralId);
const destCity = options.worldRef?.getCityById(destCityId);
if (!destGeneral || !destCity) {
return null;
}
return {
...base,
destGeneral,
destCity,
currentYear: options.world.currentYear,
currentMonth: options.world.currentMonth,
turnTermMinutes: resolveTurnTermMinutes(options.world),
generalTurnTime: base.general.turnTime,
destGeneralTurnTime: destGeneral.turnTime,
};
};
export const commandSpec: NationTurnCommandSpec = {
key: 'che_발령',
category: '인사',
@@ -15,6 +15,7 @@ import type {
} from '../../engine.js';
import { createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { ActionContextBuilder } from '../actionContext.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
@@ -161,6 +162,13 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 날짜 정보를 제공한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
...base,
currentYear: options.world.currentYear,
currentMonth: options.world.currentMonth,
});
export const commandSpec: NationTurnCommandSpec = {
key: 'che_불가침제의',
category: '외교',
@@ -16,6 +16,7 @@ import type {
import { createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { NationTurnCommandSpec } from './index.js';
export interface NonAggressionCancelProposalArgs {
@@ -86,6 +87,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: NationTurnCommandSpec = {
key: 'che_불가침파기제의',
category: '외교',
@@ -16,6 +16,7 @@ import type {
import { createDiplomacyPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { NationTurnCommandSpec } from './index.js';
export interface DeclareWarArgs {
@@ -106,6 +107,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: NationTurnCommandSpec = {
key: 'che_선전포고',
category: '외교',
@@ -32,6 +32,12 @@ import {
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { buildRecruitmentGeneral } from '../general/recruitment.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBuilder } from '../actionContext.js';
import {
buildAverageNationGeneralCount,
buildNationSummary,
resolveStartYear,
} from '../actionContextHelpers.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
@@ -419,6 +425,26 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 국가 평균 정보를 구성한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const nationSummary = buildNationSummary(
options.worldRef,
base.general.nationId
);
return {
...base,
currentYear: options.world.currentYear,
startYear: resolveStartYear(options.world, options.scenarioMeta),
averageNationGeneralCount: buildAverageNationGeneralCount(
options.worldRef
),
nationAverageStats: nationSummary.averageStats,
nationAverageExperience: nationSummary.averageExperience,
nationAverageDedication: nationSummary.averageDedication,
createGeneralId: options.createGeneralId,
};
};
export const commandSpec: NationTurnCommandSpec = {
key: 'che_의병모집',
category: '전략',
@@ -36,6 +36,7 @@ import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import type { ActionContextBuilder } from '../actionContext.js';
export interface AwardArgs {
isGold: boolean;
@@ -271,6 +272,22 @@ export class ActionDefinition<
}
}
// 예약 턴 실행에 필요한 대상 장수를 주입한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const destGeneralId = options.actionArgs.destGeneralId;
if (typeof destGeneralId !== 'number') {
return null;
}
const destGeneral = options.worldRef?.getGeneralById(destGeneralId);
if (!destGeneral) {
return null;
}
return {
...base,
destGeneral,
};
};
export const commandSpec: NationTurnCommandSpec = {
key: 'che_포상',
category: '인사',
@@ -12,6 +12,7 @@ import type {
GeneralActionResolver,
} from '../../engine.js';
import type { TurnCommandEnv } from '../commandEnv.js';
import { defaultActionContextBuilder } from '../actionContext.js';
import type { NationTurnCommandSpec } from './index.js';
export interface NationRestArgs {}
@@ -62,6 +63,9 @@ export class ActionDefinition<
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: NationTurnCommandSpec = {
key: '휴식',
category: '휴식',