feat: Implement general and nation turn commands with command specifications
- Added command specifications for various general turn commands including talent scouting, appointments, and military actions. - Introduced command specifications for nation turn commands such as assignments and declarations of war. - Created a command environment interface to encapsulate command-related parameters. - Developed a command profile loader to manage default and custom command profiles. - Established a reserved turn action context to facilitate command execution with necessary context data. - Updated the default command profile JSON to include new commands and their configurations.
This commit is contained in:
@@ -4,36 +4,18 @@ import type {
|
|||||||
ConstraintContext,
|
ConstraintContext,
|
||||||
General,
|
General,
|
||||||
GeneralActionDefinition,
|
GeneralActionDefinition,
|
||||||
|
GeneralTurnCommandSpec,
|
||||||
Nation,
|
Nation,
|
||||||
|
NationTurnCommandSpec,
|
||||||
RequirementKey,
|
RequirementKey,
|
||||||
StateView,
|
StateView,
|
||||||
|
TurnCommandEnv,
|
||||||
TriggerValue,
|
TriggerValue,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import {
|
import {
|
||||||
AppointmentActionDefinition,
|
|
||||||
AssignmentActionDefinition,
|
|
||||||
BoostMoraleActionDefinition,
|
|
||||||
AwardActionDefinition,
|
|
||||||
CommerceInvestmentActionDefinition,
|
|
||||||
DeclarationActionDefinition,
|
|
||||||
DefenceUpgradeActionDefinition,
|
|
||||||
DispatchActionDefinition,
|
|
||||||
evaluateConstraints,
|
evaluateConstraints,
|
||||||
FireAttackActionDefinition,
|
loadGeneralTurnCommandSpecs,
|
||||||
FarmingActionDefinition,
|
loadNationTurnCommandSpecs,
|
||||||
FoundingActionDefinition,
|
|
||||||
NationRestActionDefinition,
|
|
||||||
RecoveryActionDefinition,
|
|
||||||
ResidentsSelectionActionDefinition,
|
|
||||||
RecruitActionDefinition,
|
|
||||||
RestActionDefinition,
|
|
||||||
SecurityUpgradeActionDefinition,
|
|
||||||
TechResearchActionDefinition,
|
|
||||||
TalentScoutActionDefinition,
|
|
||||||
TrainingActionDefinition,
|
|
||||||
UprisingActionDefinition,
|
|
||||||
VolunteerRecruitActionDefinition,
|
|
||||||
WallRepairActionDefinition,
|
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -42,6 +24,7 @@ import type {
|
|||||||
NationRow,
|
NationRow,
|
||||||
WorldStateRow,
|
WorldStateRow,
|
||||||
} from '../context.js';
|
} from '../context.js';
|
||||||
|
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
||||||
|
|
||||||
type AvailabilityStatus = 'available' | 'blocked' | 'needsInput' | 'unknown';
|
type AvailabilityStatus = 'available' | 'blocked' | 'needsInput' | 'unknown';
|
||||||
|
|
||||||
@@ -64,30 +47,7 @@ export interface TurnCommandTable {
|
|||||||
nation: TurnCommandGroup[];
|
nation: TurnCommandGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommandEnv {
|
type CommandEnv = TurnCommandEnv;
|
||||||
develCost: number;
|
|
||||||
trainDelta: number;
|
|
||||||
atmosDelta: number;
|
|
||||||
maxTrainByCommand: number;
|
|
||||||
maxAtmosByCommand: number;
|
|
||||||
sabotageDefaultProb: number;
|
|
||||||
sabotageProbCoefByStat: number;
|
|
||||||
sabotageDefenceCoefByGeneralCount: number;
|
|
||||||
sabotageDamageMin: number;
|
|
||||||
sabotageDamageMax: number;
|
|
||||||
openingPartYear: number;
|
|
||||||
maxGeneral: number;
|
|
||||||
defaultNpcGold: number;
|
|
||||||
defaultNpcRice: number;
|
|
||||||
defaultCrewTypeId: number;
|
|
||||||
defaultSpecialDomestic: string | null;
|
|
||||||
defaultSpecialWar: string | null;
|
|
||||||
initialNationGenLimit: number;
|
|
||||||
maxTechLevel: number;
|
|
||||||
baseGold: number;
|
|
||||||
baseRice: number;
|
|
||||||
maxResourceActionAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AvailabilityCore {
|
interface AvailabilityCore {
|
||||||
possible: boolean;
|
possible: boolean;
|
||||||
@@ -482,226 +442,47 @@ const pickAvailability = (
|
|||||||
? lhs
|
? lhs
|
||||||
: rhs;
|
: rhs;
|
||||||
|
|
||||||
const buildEntries = (env: CommandEnv): {
|
type TurnCommandSpec = GeneralTurnCommandSpec | NationTurnCommandSpec;
|
||||||
general: CommandEntry[];
|
|
||||||
nation: CommandEntry[];
|
|
||||||
} => {
|
|
||||||
const emptyModules: [] = [];
|
|
||||||
|
|
||||||
const generalEntries: CommandEntry[] = [
|
const buildEntries = (
|
||||||
{
|
env: CommandEnv,
|
||||||
category: '개인',
|
specs: TurnCommandSpec[]
|
||||||
definition: new RestActionDefinition(),
|
): CommandEntry[] => {
|
||||||
reqArg: false,
|
const entries: CommandEntry[] = [];
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '개인',
|
|
||||||
definition: new RecoveryActionDefinition(),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '전략',
|
|
||||||
definition: new UprisingActionDefinition(),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '전략',
|
|
||||||
definition: new AppointmentActionDefinition(),
|
|
||||||
reqArg: true,
|
|
||||||
args: { destNationId: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '전략',
|
|
||||||
definition: new FoundingActionDefinition(),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '군사',
|
|
||||||
definition: new TrainingActionDefinition({
|
|
||||||
trainDelta: env.trainDelta,
|
|
||||||
maxTrainByCommand: env.maxTrainByCommand,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '군사',
|
|
||||||
definition: new BoostMoraleActionDefinition({
|
|
||||||
atmosDelta: env.atmosDelta,
|
|
||||||
maxAtmosByCommand: env.maxAtmosByCommand,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '군사',
|
|
||||||
definition: new DispatchActionDefinition(),
|
|
||||||
reqArg: true,
|
|
||||||
args: { destCityId: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new ResidentsSelectionActionDefinition({
|
|
||||||
develCost: env.develCost,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new FarmingActionDefinition({
|
|
||||||
develCost: env.develCost,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new CommerceInvestmentActionDefinition(emptyModules, {
|
|
||||||
develCost: env.develCost,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new TechResearchActionDefinition({
|
|
||||||
costGold: env.develCost,
|
|
||||||
maxTechLevel: env.maxTechLevel,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new SecurityUpgradeActionDefinition({
|
|
||||||
develCost: env.develCost,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new DefenceUpgradeActionDefinition({
|
|
||||||
develCost: env.develCost,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new WallRepairActionDefinition({
|
|
||||||
develCost: env.develCost,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '내정',
|
|
||||||
definition: new RecruitActionDefinition(emptyModules, {}),
|
|
||||||
reqArg: true,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '계략',
|
|
||||||
definition: new FireAttackActionDefinition(emptyModules, {
|
|
||||||
develCost: env.develCost,
|
|
||||||
sabotageDefaultProb: env.sabotageDefaultProb,
|
|
||||||
sabotageProbCoefByStat: env.sabotageProbCoefByStat,
|
|
||||||
sabotageDefenceCoefByGeneralCount:
|
|
||||||
env.sabotageDefenceCoefByGeneralCount,
|
|
||||||
sabotageDamageMin: env.sabotageDamageMin,
|
|
||||||
sabotageDamageMax: env.sabotageDamageMax,
|
|
||||||
}),
|
|
||||||
reqArg: true,
|
|
||||||
args: { destCityId: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '인사',
|
|
||||||
definition: new TalentScoutActionDefinition(emptyModules, {
|
|
||||||
develCost: env.develCost,
|
|
||||||
maxGeneral: env.maxGeneral,
|
|
||||||
defaultNpcGold: env.defaultNpcGold,
|
|
||||||
defaultNpcRice: env.defaultNpcRice,
|
|
||||||
defaultCrewTypeId: env.defaultCrewTypeId,
|
|
||||||
defaultSpecialDomestic: env.defaultSpecialDomestic,
|
|
||||||
defaultSpecialWar: env.defaultSpecialWar,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '전략',
|
|
||||||
definition: new VolunteerRecruitActionDefinition(emptyModules, {
|
|
||||||
openingPartYear: env.openingPartYear,
|
|
||||||
initialNationGenLimit: env.initialNationGenLimit,
|
|
||||||
defaultNpcGold: env.defaultNpcGold,
|
|
||||||
defaultNpcRice: env.defaultNpcRice,
|
|
||||||
defaultCrewTypeId: env.defaultCrewTypeId,
|
|
||||||
defaultSpecialDomestic: env.defaultSpecialDomestic,
|
|
||||||
defaultSpecialWar: env.defaultSpecialWar,
|
|
||||||
}),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const awardDefinition = new AwardActionDefinition({
|
for (const spec of specs) {
|
||||||
baseGold: env.baseGold,
|
const definition = spec.createDefinition(env);
|
||||||
baseRice: env.baseRice,
|
const entry: CommandEntry = {
|
||||||
maxAmount: env.maxResourceActionAmount,
|
category: spec.category,
|
||||||
});
|
definition,
|
||||||
const assignmentDefinition = new AssignmentActionDefinition({});
|
reqArg: spec.reqArg,
|
||||||
|
args: spec.args,
|
||||||
|
};
|
||||||
|
|
||||||
const nationEntries: CommandEntry[] = [
|
if (spec.key === 'che_포상') {
|
||||||
{
|
entry.evaluate = (ctx, view) => {
|
||||||
category: '휴식',
|
|
||||||
definition: new NationRestActionDefinition(),
|
|
||||||
reqArg: false,
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '외교',
|
|
||||||
definition: new DeclarationActionDefinition(),
|
|
||||||
reqArg: true,
|
|
||||||
args: { destNationId: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '인사',
|
|
||||||
definition: assignmentDefinition,
|
|
||||||
reqArg: true,
|
|
||||||
args: { destGeneralId: 0, destCityId: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: '인사',
|
|
||||||
definition: awardDefinition,
|
|
||||||
reqArg: true,
|
|
||||||
args: { isGold: true, amount: 1, destGeneralId: 0 },
|
|
||||||
evaluate: (ctx, view) => {
|
|
||||||
const gold = evaluateDefinition(
|
const gold = evaluateDefinition(
|
||||||
awardDefinition,
|
definition,
|
||||||
ctx,
|
ctx,
|
||||||
view,
|
view,
|
||||||
true,
|
true,
|
||||||
{ isGold: true, amount: 1, destGeneralId: 0 }
|
{ isGold: true, amount: 1, destGeneralId: 0 }
|
||||||
);
|
);
|
||||||
const rice = evaluateDefinition(
|
const rice = evaluateDefinition(
|
||||||
awardDefinition,
|
definition,
|
||||||
ctx,
|
ctx,
|
||||||
view,
|
view,
|
||||||
true,
|
true,
|
||||||
{ isGold: false, amount: 1, destGeneralId: 0 }
|
{ isGold: false, amount: 1, destGeneralId: 0 }
|
||||||
);
|
);
|
||||||
return pickAvailability(gold, rice);
|
return pickAvailability(gold, rice);
|
||||||
},
|
};
|
||||||
},
|
}
|
||||||
];
|
|
||||||
|
|
||||||
return { general: generalEntries, nation: nationEntries };
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildGroups = (
|
const buildGroups = (
|
||||||
@@ -742,12 +523,12 @@ const buildGroups = (
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildTurnCommandTable = (options: {
|
export const buildTurnCommandTable = async (options: {
|
||||||
worldState: WorldStateRow;
|
worldState: WorldStateRow;
|
||||||
general: GeneralRow;
|
general: GeneralRow;
|
||||||
city: CityRow | null;
|
city: CityRow | null;
|
||||||
nation: NationRow | null;
|
nation: NationRow | null;
|
||||||
}): TurnCommandTable => {
|
}): Promise<TurnCommandTable> => {
|
||||||
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
||||||
const general = mapGeneralRow(options.general);
|
const general = mapGeneralRow(options.general);
|
||||||
const city = options.city ? mapCityRow(options.city) : null;
|
const city = options.city ? mapCityRow(options.city) : null;
|
||||||
@@ -764,10 +545,16 @@ export const buildTurnCommandTable = (options: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const env = buildCommandEnv(options.worldState);
|
const env = buildCommandEnv(options.worldState);
|
||||||
const entries = buildEntries(env);
|
const commandProfile = await loadTurnCommandProfile();
|
||||||
|
const [generalSpecs, nationSpecs] = await Promise.all([
|
||||||
|
loadGeneralTurnCommandSpecs(commandProfile.general),
|
||||||
|
loadNationTurnCommandSpecs(commandProfile.nation),
|
||||||
|
]);
|
||||||
|
const generalEntries = buildEntries(env, generalSpecs);
|
||||||
|
const nationEntries = buildEntries(env, nationSpecs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
general: buildGroups(entries.general, ctx, view),
|
general: buildGroups(generalEntries, ctx, view),
|
||||||
nation: buildGroups(entries.nation, ctx, view),
|
nation: buildGroups(nationEntries, ctx, view),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_TURN_COMMAND_PROFILE,
|
||||||
|
parseTurnCommandProfile,
|
||||||
|
type TurnCommandProfile,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||||
|
const DEFAULT_PROFILE_PATH = path.resolve(
|
||||||
|
REPO_ROOT,
|
||||||
|
'resources',
|
||||||
|
'turn-commands',
|
||||||
|
'default.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface TurnCommandProfileOptions {
|
||||||
|
filePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readCommandProfile = async (filePath: string): Promise<TurnCommandProfile> => {
|
||||||
|
const raw = await fs.readFile(filePath, 'utf8');
|
||||||
|
return parseTurnCommandProfile(JSON.parse(raw) as unknown);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTurnCommandProfile = async (
|
||||||
|
options?: TurnCommandProfileOptions
|
||||||
|
): Promise<TurnCommandProfile> => {
|
||||||
|
const filePath =
|
||||||
|
options?.filePath ?? process.env.TURN_COMMANDS_PATH ?? DEFAULT_PROFILE_PATH;
|
||||||
|
try {
|
||||||
|
return await readCommandProfile(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
|
return DEFAULT_TURN_COMMAND_PROFILE;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import type {
|
||||||
|
City,
|
||||||
|
General,
|
||||||
|
MapDefinition,
|
||||||
|
Nation,
|
||||||
|
ScenarioMeta,
|
||||||
|
UnitSetDefinition,
|
||||||
|
} 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;
|
||||||
|
scenarioMeta?: ScenarioMeta;
|
||||||
|
map?: MapDefinition;
|
||||||
|
unitSet?: UnitSetDefinition;
|
||||||
|
worldRef: InMemoryTurnWorld | null;
|
||||||
|
actionArgs: Record<string, unknown>;
|
||||||
|
createGeneralId: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildActionContext = (
|
||||||
|
key: string,
|
||||||
|
base: ActionContextBase,
|
||||||
|
options: ActionContextOptions
|
||||||
|
): ActionResolveContext | null => {
|
||||||
|
const builder = ACTION_CONTEXT_BUILDERS[key];
|
||||||
|
return builder ? builder(base, options) : base;
|
||||||
|
};
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import type {
|
||||||
|
GeneralActionDefinition,
|
||||||
|
GeneralTurnCommandKey,
|
||||||
|
NationTurnCommandKey,
|
||||||
|
ScenarioConfig,
|
||||||
|
TurnCommandEnv,
|
||||||
|
TurnCommandProfile,
|
||||||
|
UnitSetDefinition,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
|
import {
|
||||||
|
loadGeneralTurnCommandSpecs,
|
||||||
|
loadNationTurnCommandSpecs,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
const DEFAULT_GENERAL_GOLD = 1000;
|
||||||
|
const DEFAULT_GENERAL_RICE = 1000;
|
||||||
|
const DEFAULT_CREW_TYPE_ID = 1100;
|
||||||
|
|
||||||
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
|
||||||
|
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||||
|
isRecord(value) ? value : {};
|
||||||
|
|
||||||
|
const normalizeCode = (value: string | null | undefined): string | null => {
|
||||||
|
if (!value || value === 'None') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveNumber = (
|
||||||
|
source: Record<string, unknown>,
|
||||||
|
keys: string[],
|
||||||
|
fallback: number
|
||||||
|
): number => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = source[key];
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveOptionalString = (
|
||||||
|
source: Record<string, unknown>,
|
||||||
|
keys: string[]
|
||||||
|
): string | null => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = source[key];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return normalizeCode(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildCommandEnv = (
|
||||||
|
config: ScenarioConfig,
|
||||||
|
unitSet?: UnitSetDefinition
|
||||||
|
): TurnCommandEnv => {
|
||||||
|
const constValues = asRecord(config.const);
|
||||||
|
|
||||||
|
return {
|
||||||
|
develCost: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['develCost', 'develcost', 'develrate'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
trainDelta: resolveNumber(constValues, ['trainDelta'], 0),
|
||||||
|
atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0),
|
||||||
|
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], 0),
|
||||||
|
maxAtmosByCommand: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['maxAtmosByCommand'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
sabotageDefaultProb: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['sabotageDefaultProb'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
sabotageProbCoefByStat: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['sabotageProbCoefByStat'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
sabotageDefenceCoefByGeneralCount: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['sabotageDefenceCoefByGeneralCount'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
sabotageDamageMin: resolveNumber(constValues, ['sabotageDamageMin'], 0),
|
||||||
|
sabotageDamageMax: resolveNumber(constValues, ['sabotageDamageMax'], 0),
|
||||||
|
openingPartYear: resolveNumber(constValues, ['openingPartYear'], 0),
|
||||||
|
maxGeneral: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['defaultMaxGeneral', 'maxGeneral'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
defaultNpcGold: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['defaultNpcGold', 'defaultGold'],
|
||||||
|
DEFAULT_GENERAL_GOLD
|
||||||
|
),
|
||||||
|
defaultNpcRice: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['defaultNpcRice', 'defaultRice'],
|
||||||
|
DEFAULT_GENERAL_RICE
|
||||||
|
),
|
||||||
|
defaultCrewTypeId: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['defaultCrewTypeId'],
|
||||||
|
unitSet?.defaultCrewTypeId ?? DEFAULT_CREW_TYPE_ID
|
||||||
|
),
|
||||||
|
defaultSpecialDomestic: resolveOptionalString(
|
||||||
|
constValues,
|
||||||
|
['defaultSpecialDomestic']
|
||||||
|
),
|
||||||
|
defaultSpecialWar: resolveOptionalString(constValues, ['defaultSpecialWar']),
|
||||||
|
initialNationGenLimit: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['initialNationGenLimit'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||||
|
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||||
|
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||||
|
maxResourceActionAmount: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['maxResourceActionAmount'],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureGeneralFallback = async (
|
||||||
|
definitions: Map<string, GeneralActionDefinition>,
|
||||||
|
fallbackKey: GeneralTurnCommandKey,
|
||||||
|
env: TurnCommandEnv
|
||||||
|
): Promise<void> => {
|
||||||
|
if (definitions.has(fallbackKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [spec] = await loadGeneralTurnCommandSpecs([fallbackKey]);
|
||||||
|
if (!spec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
definitions.set(fallbackKey, spec.createDefinition(env));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildReservedTurnDefinitions = async (options: {
|
||||||
|
env: TurnCommandEnv;
|
||||||
|
commandProfile: TurnCommandProfile;
|
||||||
|
defaultActionKey: GeneralTurnCommandKey & NationTurnCommandKey;
|
||||||
|
}): Promise<{
|
||||||
|
general: Map<string, GeneralActionDefinition>;
|
||||||
|
nation: Map<string, GeneralActionDefinition>;
|
||||||
|
}> => {
|
||||||
|
const generalSpecs = await loadGeneralTurnCommandSpecs(
|
||||||
|
options.commandProfile.general
|
||||||
|
);
|
||||||
|
const nationSpecs = await loadNationTurnCommandSpecs(
|
||||||
|
options.commandProfile.nation
|
||||||
|
);
|
||||||
|
|
||||||
|
const general = new Map(
|
||||||
|
generalSpecs.map((spec) => [spec.key, spec.createDefinition(options.env)])
|
||||||
|
);
|
||||||
|
const nation = new Map(
|
||||||
|
nationSpecs.map((spec) => [spec.key, spec.createDefinition(options.env)])
|
||||||
|
);
|
||||||
|
|
||||||
|
await ensureGeneralFallback(general, options.defaultActionKey, options.env);
|
||||||
|
if (!nation.has(options.defaultActionKey)) {
|
||||||
|
const [spec] = await loadNationTurnCommandSpecs([
|
||||||
|
options.defaultActionKey,
|
||||||
|
]);
|
||||||
|
if (spec) {
|
||||||
|
nation.set(spec.key, spec.createDefinition(options.env));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { general, nation };
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
City,
|
City,
|
||||||
General,
|
|
||||||
GeneralActionDefinition,
|
GeneralActionDefinition,
|
||||||
GeneralActionResolveContext,
|
GeneralActionResolveContext,
|
||||||
LogEntryDraft,
|
LogEntryDraft,
|
||||||
@@ -10,34 +9,13 @@ import type {
|
|||||||
ScenarioDiplomacy,
|
ScenarioDiplomacy,
|
||||||
ScenarioMeta,
|
ScenarioMeta,
|
||||||
Troop,
|
Troop,
|
||||||
|
TurnCommandProfile,
|
||||||
UnitSetDefinition,
|
UnitSetDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import {
|
import {
|
||||||
AppointmentActionDefinition,
|
DEFAULT_TURN_COMMAND_PROFILE,
|
||||||
AssignmentActionDefinition,
|
|
||||||
BoostMoraleActionDefinition,
|
|
||||||
AwardActionDefinition,
|
|
||||||
CommerceInvestmentActionDefinition,
|
|
||||||
DeclarationActionDefinition,
|
|
||||||
DefenceUpgradeActionDefinition,
|
|
||||||
DispatchActionDefinition,
|
|
||||||
evaluateConstraints,
|
evaluateConstraints,
|
||||||
FireAttackActionDefinition,
|
|
||||||
FarmingActionDefinition,
|
|
||||||
FoundingActionDefinition,
|
|
||||||
NationRestActionDefinition,
|
|
||||||
RecoveryActionDefinition,
|
|
||||||
ResidentsSelectionActionDefinition,
|
|
||||||
RecruitActionDefinition,
|
|
||||||
resolveGeneralAction,
|
resolveGeneralAction,
|
||||||
RestActionDefinition,
|
|
||||||
SecurityUpgradeActionDefinition,
|
|
||||||
TechResearchActionDefinition,
|
|
||||||
TalentScoutActionDefinition,
|
|
||||||
TrainingActionDefinition,
|
|
||||||
UprisingActionDefinition,
|
|
||||||
VolunteerRecruitActionDefinition,
|
|
||||||
WallRepairActionDefinition,
|
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||||
import { LiteHashDRBG } from '@sammo-ts/common';
|
import { LiteHashDRBG } from '@sammo-ts/common';
|
||||||
@@ -49,47 +27,13 @@ import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
|||||||
import type { TurnGeneral, TurnWorldState } from './types.js';
|
import type { TurnGeneral, TurnWorldState } from './types.js';
|
||||||
import type { ReservedTurnEntry } from './reservedTurnStore.js';
|
import type { ReservedTurnEntry } from './reservedTurnStore.js';
|
||||||
import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
||||||
|
import {
|
||||||
|
buildCommandEnv,
|
||||||
|
buildReservedTurnDefinitions,
|
||||||
|
} from './reservedTurnCommands.js';
|
||||||
|
import type { ActionContextBase } from './reservedTurnActionContext.js';
|
||||||
|
import { buildActionContext } from './reservedTurnActionContext.js';
|
||||||
|
|
||||||
interface CommandEnv {
|
|
||||||
develCost: number;
|
|
||||||
trainDelta: number;
|
|
||||||
atmosDelta: number;
|
|
||||||
maxTrainByCommand: number;
|
|
||||||
maxAtmosByCommand: number;
|
|
||||||
sabotageDefaultProb: number;
|
|
||||||
sabotageProbCoefByStat: number;
|
|
||||||
sabotageDefenceCoefByGeneralCount: number;
|
|
||||||
sabotageDamageMin: number;
|
|
||||||
sabotageDamageMax: number;
|
|
||||||
openingPartYear: number;
|
|
||||||
maxGeneral: number;
|
|
||||||
defaultNpcGold: number;
|
|
||||||
defaultNpcRice: number;
|
|
||||||
defaultCrewTypeId: number;
|
|
||||||
defaultSpecialDomestic: string | null;
|
|
||||||
defaultSpecialWar: string | null;
|
|
||||||
initialNationGenLimit: number;
|
|
||||||
maxTechLevel: number;
|
|
||||||
baseGold: number;
|
|
||||||
baseRice: number;
|
|
||||||
maxResourceActionAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorldSummary {
|
|
||||||
totalGeneralCount: number;
|
|
||||||
totalNpcCount: number;
|
|
||||||
averageStats?: General['stats'];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NationSummary {
|
|
||||||
averageStats?: General['stats'];
|
|
||||||
averageExperience?: number;
|
|
||||||
averageDedication?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_GENERAL_GOLD = 1000;
|
|
||||||
const DEFAULT_GENERAL_RICE = 1000;
|
|
||||||
const DEFAULT_CREW_TYPE_ID = 1100;
|
|
||||||
const DEFAULT_ACTION = '휴식';
|
const DEFAULT_ACTION = '휴식';
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
@@ -98,134 +42,6 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|||||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||||
isRecord(value) ? value : {};
|
isRecord(value) ? value : {};
|
||||||
|
|
||||||
const normalizeCode = (value: string | null | undefined): string | null => {
|
|
||||||
if (!value || value === 'None') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveNumber = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
keys: string[],
|
|
||||||
fallback: number
|
|
||||||
): number => {
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = source[key];
|
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveOptionalString = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
keys: string[]
|
|
||||||
): string | null => {
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = source[key];
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return normalizeCode(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildCommandEnv = (
|
|
||||||
config: ScenarioConfig,
|
|
||||||
unitSet?: UnitSetDefinition
|
|
||||||
): CommandEnv => {
|
|
||||||
const constValues = asRecord(config.const);
|
|
||||||
|
|
||||||
return {
|
|
||||||
develCost: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['develCost', 'develcost', 'develrate'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
trainDelta: resolveNumber(constValues, ['trainDelta'], 0),
|
|
||||||
atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0),
|
|
||||||
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], 0),
|
|
||||||
maxAtmosByCommand: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['maxAtmosByCommand'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
sabotageDefaultProb: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['sabotageDefaultProb'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
sabotageProbCoefByStat: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['sabotageProbCoefByStat'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
sabotageDefenceCoefByGeneralCount: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['sabotageDefenceCoefByGeneralCount'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
sabotageDamageMin: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['sabotageDamageMin'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
sabotageDamageMax: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['sabotageDamageMax'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
openingPartYear: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['openingPartYear'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
maxGeneral: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['defaultMaxGeneral', 'maxGeneral'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
defaultNpcGold: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['defaultNpcGold', 'defaultGold'],
|
|
||||||
DEFAULT_GENERAL_GOLD
|
|
||||||
),
|
|
||||||
defaultNpcRice: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['defaultNpcRice', 'defaultRice'],
|
|
||||||
DEFAULT_GENERAL_RICE
|
|
||||||
),
|
|
||||||
defaultCrewTypeId: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['defaultCrewTypeId'],
|
|
||||||
unitSet?.defaultCrewTypeId ?? DEFAULT_CREW_TYPE_ID
|
|
||||||
),
|
|
||||||
defaultSpecialDomestic: resolveOptionalString(
|
|
||||||
constValues,
|
|
||||||
['defaultSpecialDomestic']
|
|
||||||
),
|
|
||||||
defaultSpecialWar: resolveOptionalString(
|
|
||||||
constValues,
|
|
||||||
['defaultSpecialWar']
|
|
||||||
),
|
|
||||||
initialNationGenLimit: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['initialNationGenLimit'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
|
||||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
|
||||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
|
||||||
maxResourceActionAmount: resolveNumber(
|
|
||||||
constValues,
|
|
||||||
['maxResourceActionAmount'],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveConstraintEnv = (
|
const resolveConstraintEnv = (
|
||||||
world: TurnWorldState,
|
world: TurnWorldState,
|
||||||
scenarioMeta?: ScenarioMeta
|
scenarioMeta?: ScenarioMeta
|
||||||
@@ -259,93 +75,6 @@ const buildDiplomacyMap = (
|
|||||||
return map;
|
return map;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 buildSeedBase = (world: TurnWorldState): string => {
|
const buildSeedBase = (world: TurnWorldState): string => {
|
||||||
const meta = asRecord(world.meta);
|
const meta = asRecord(world.meta);
|
||||||
@@ -445,196 +174,12 @@ class WorldStateView implements StateView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildGeneralDefinitions = (
|
|
||||||
env: CommandEnv
|
|
||||||
): Map<string, GeneralActionDefinition> => {
|
|
||||||
const definitions = new Map<string, GeneralActionDefinition>();
|
|
||||||
definitions.set('che_거병', new UprisingActionDefinition());
|
|
||||||
definitions.set('che_임관', new AppointmentActionDefinition());
|
|
||||||
definitions.set('che_건국', new FoundingActionDefinition());
|
|
||||||
definitions.set(
|
|
||||||
'che_훈련',
|
|
||||||
new TrainingActionDefinition({
|
|
||||||
trainDelta: env.trainDelta,
|
|
||||||
maxTrainByCommand: env.maxTrainByCommand,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_사기진작',
|
|
||||||
new BoostMoraleActionDefinition({
|
|
||||||
atmosDelta: env.atmosDelta,
|
|
||||||
maxAtmosByCommand: env.maxAtmosByCommand,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
definitions.set('che_요양', new RecoveryActionDefinition());
|
|
||||||
definitions.set('che_출병', new DispatchActionDefinition());
|
|
||||||
definitions.set(
|
|
||||||
'che_주민선정',
|
|
||||||
new ResidentsSelectionActionDefinition({ develCost: env.develCost })
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_농지개간',
|
|
||||||
new FarmingActionDefinition({ develCost: env.develCost })
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_상업투자',
|
|
||||||
new CommerceInvestmentActionDefinition([], env)
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_기술연구',
|
|
||||||
new TechResearchActionDefinition({
|
|
||||||
costGold: env.develCost,
|
|
||||||
maxTechLevel: env.maxTechLevel,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_치안강화',
|
|
||||||
new SecurityUpgradeActionDefinition({ develCost: env.develCost })
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_수비강화',
|
|
||||||
new DefenceUpgradeActionDefinition({ develCost: env.develCost })
|
|
||||||
);
|
|
||||||
definitions.set(
|
|
||||||
'che_성벽보수',
|
|
||||||
new WallRepairActionDefinition({ develCost: env.develCost })
|
|
||||||
);
|
|
||||||
definitions.set('che_화계', new FireAttackActionDefinition([], env));
|
|
||||||
definitions.set('che_인재탐색', new TalentScoutActionDefinition([], env));
|
|
||||||
definitions.set('che_의병모집', new VolunteerRecruitActionDefinition([], env));
|
|
||||||
definitions.set('che_징병', new RecruitActionDefinition([], {}));
|
|
||||||
definitions.set('휴식', new RestActionDefinition());
|
|
||||||
return definitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildNationDefinitions = (
|
|
||||||
env: CommandEnv
|
|
||||||
): Map<string, GeneralActionDefinition> => {
|
|
||||||
const definitions = new Map<string, GeneralActionDefinition>();
|
|
||||||
const maxAmount =
|
|
||||||
env.maxResourceActionAmount > 0
|
|
||||||
? env.maxResourceActionAmount
|
|
||||||
: Math.max(env.baseGold, env.baseRice, 1000);
|
|
||||||
definitions.set('휴식', new NationRestActionDefinition());
|
|
||||||
definitions.set('che_선전포고', new DeclarationActionDefinition());
|
|
||||||
definitions.set(
|
|
||||||
'che_포상',
|
|
||||||
new AwardActionDefinition({
|
|
||||||
baseGold: env.baseGold,
|
|
||||||
baseRice: env.baseRice,
|
|
||||||
maxAmount,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
definitions.set('che_발령', new AssignmentActionDefinition({}));
|
|
||||||
return definitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractArgsRecord = (value: unknown): Record<string, unknown> =>
|
const extractArgsRecord = (value: unknown): Record<string, unknown> =>
|
||||||
isRecord(value) ? value : {};
|
isRecord(value) ? value : {};
|
||||||
|
|
||||||
const resolveTurnTermMinutes = (world: TurnWorldState): number =>
|
|
||||||
Math.max(1, Math.round(world.tickSeconds / 60));
|
|
||||||
|
|
||||||
type ActionContextBase = {
|
|
||||||
general: TurnGeneral;
|
|
||||||
city?: City;
|
|
||||||
nation?: Nation | null;
|
|
||||||
rng: DeterministicRandom;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActionResolveContext = ActionContextBase & Record<string, unknown>;
|
|
||||||
|
|
||||||
const buildActionContext = (
|
|
||||||
key: string,
|
|
||||||
base: ActionContextBase,
|
|
||||||
options: {
|
|
||||||
world: TurnWorldState;
|
|
||||||
scenarioMeta?: ScenarioMeta;
|
|
||||||
map?: MapDefinition;
|
|
||||||
unitSet?: UnitSetDefinition;
|
|
||||||
worldRef: InMemoryTurnWorld | null;
|
|
||||||
actionArgs: Record<string, unknown>;
|
|
||||||
createGeneralId: () => number;
|
|
||||||
}
|
|
||||||
): ActionResolveContext | null => {
|
|
||||||
switch (key) {
|
|
||||||
case 'che_인재탐색':
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
currentYear: options.world.currentYear,
|
|
||||||
worldSummary: buildWorldSummary(options.worldRef),
|
|
||||||
createGeneralId: options.createGeneralId,
|
|
||||||
};
|
|
||||||
case 'che_의병모집': {
|
|
||||||
const nationSummary = buildNationSummary(
|
|
||||||
options.worldRef,
|
|
||||||
(base.general as TurnGeneral).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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'che_포상': {
|
|
||||||
const destGeneralId = options.actionArgs.destGeneralId;
|
|
||||||
if (typeof destGeneralId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const destGeneral = options.worldRef?.getGeneralById(destGeneralId);
|
|
||||||
if (!destGeneral) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
destGeneral,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'che_발령': {
|
|
||||||
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 as TurnGeneral).turnTime,
|
|
||||||
destGeneralTurnTime: destGeneral.turnTime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'che_징병':
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildConstraintContext = (
|
const buildConstraintContext = (
|
||||||
general: TurnGeneral,
|
general: TurnGeneral,
|
||||||
@@ -664,7 +209,7 @@ const resolveDefinition = (
|
|||||||
fallback: GeneralActionDefinition
|
fallback: GeneralActionDefinition
|
||||||
): GeneralActionDefinition => definitions.get(actionKey) ?? fallback;
|
): GeneralActionDefinition => definitions.get(actionKey) ?? fallback;
|
||||||
|
|
||||||
export const createReservedTurnHandler = (options: {
|
export const createReservedTurnHandler = async (options: {
|
||||||
reservedTurns: InMemoryReservedTurnStore;
|
reservedTurns: InMemoryReservedTurnStore;
|
||||||
scenarioConfig: ScenarioConfig;
|
scenarioConfig: ScenarioConfig;
|
||||||
scenarioMeta?: ScenarioMeta;
|
scenarioMeta?: ScenarioMeta;
|
||||||
@@ -672,10 +217,17 @@ export const createReservedTurnHandler = (options: {
|
|||||||
map?: MapDefinition;
|
map?: MapDefinition;
|
||||||
unitSet?: UnitSetDefinition;
|
unitSet?: UnitSetDefinition;
|
||||||
getWorld: () => InMemoryTurnWorld | null;
|
getWorld: () => InMemoryTurnWorld | null;
|
||||||
}): GeneralTurnHandler => {
|
commandProfile?: TurnCommandProfile;
|
||||||
|
}): Promise<GeneralTurnHandler> => {
|
||||||
const env = buildCommandEnv(options.scenarioConfig, options.unitSet);
|
const env = buildCommandEnv(options.scenarioConfig, options.unitSet);
|
||||||
const generalDefinitions = buildGeneralDefinitions(env);
|
const commandProfile =
|
||||||
const nationDefinitions = buildNationDefinitions(env);
|
options.commandProfile ?? DEFAULT_TURN_COMMAND_PROFILE;
|
||||||
|
const { general: generalDefinitions, nation: nationDefinitions } =
|
||||||
|
await buildReservedTurnDefinitions({
|
||||||
|
env,
|
||||||
|
commandProfile,
|
||||||
|
defaultActionKey: DEFAULT_ACTION,
|
||||||
|
});
|
||||||
const generalFallback = generalDefinitions.get(DEFAULT_ACTION)!;
|
const generalFallback = generalDefinitions.get(DEFAULT_ACTION)!;
|
||||||
const nationFallback = nationDefinitions.get(DEFAULT_ACTION)!;
|
const nationFallback = nationDefinitions.get(DEFAULT_ACTION)!;
|
||||||
const diplomacyMap = buildDiplomacyMap(options.diplomacy);
|
const diplomacyMap = buildDiplomacyMap(options.diplomacy);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_TURN_COMMAND_PROFILE,
|
||||||
|
parseTurnCommandProfile,
|
||||||
|
type TurnCommandProfile,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||||
|
const DEFAULT_PROFILE_PATH = path.resolve(
|
||||||
|
REPO_ROOT,
|
||||||
|
'resources',
|
||||||
|
'turn-commands',
|
||||||
|
'default.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface TurnCommandProfileOptions {
|
||||||
|
filePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readCommandProfile = async (filePath: string): Promise<TurnCommandProfile> => {
|
||||||
|
const raw = await fs.readFile(filePath, 'utf8');
|
||||||
|
return parseTurnCommandProfile(JSON.parse(raw) as unknown);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTurnCommandProfile = async (
|
||||||
|
options?: TurnCommandProfileOptions
|
||||||
|
): Promise<TurnCommandProfile> => {
|
||||||
|
const filePath =
|
||||||
|
options?.filePath ?? process.env.TURN_COMMANDS_PATH ?? DEFAULT_PROFILE_PATH;
|
||||||
|
try {
|
||||||
|
return await readCommandProfile(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
|
return DEFAULT_TURN_COMMAND_PROFILE;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TurnSchedule } from '@sammo-ts/logic';
|
import type { TurnCommandProfile, TurnSchedule } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import { SystemClock } from '../lifecycle/clock.js';
|
import { SystemClock } from '../lifecycle/clock.js';
|
||||||
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
||||||
@@ -23,6 +23,7 @@ import { InMemoryTurnStateStore } from './inMemoryStateStore.js';
|
|||||||
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
||||||
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
||||||
import { createReservedTurnStore } from './reservedTurnStore.js';
|
import { createReservedTurnStore } from './reservedTurnStore.js';
|
||||||
|
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
||||||
import { loadTurnWorldFromDatabase } from './worldLoader.js';
|
import { loadTurnWorldFromDatabase } from './worldLoader.js';
|
||||||
|
|
||||||
export interface TurnDaemonRuntimeOptions {
|
export interface TurnDaemonRuntimeOptions {
|
||||||
@@ -39,6 +40,8 @@ export interface TurnDaemonRuntimeOptions {
|
|||||||
calendarHandler?: TurnCalendarHandler;
|
calendarHandler?: TurnCalendarHandler;
|
||||||
enableDatabaseFlush?: boolean;
|
enableDatabaseFlush?: boolean;
|
||||||
pauseGateIntervalMs?: number;
|
pauseGateIntervalMs?: number;
|
||||||
|
commandProfile?: TurnCommandProfile;
|
||||||
|
commandProfilePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TurnDaemonRuntime {
|
export interface TurnDaemonRuntime {
|
||||||
@@ -81,12 +84,19 @@ export const createTurnDaemonRuntime = async (
|
|||||||
: await createReservedTurnStore({
|
: await createReservedTurnStore({
|
||||||
databaseUrl: options.databaseUrl,
|
databaseUrl: options.databaseUrl,
|
||||||
});
|
});
|
||||||
|
const commandProfile =
|
||||||
|
options.commandProfile ??
|
||||||
|
(options.commandProfilePath
|
||||||
|
? await loadTurnCommandProfile({
|
||||||
|
filePath: options.commandProfilePath,
|
||||||
|
})
|
||||||
|
: await loadTurnCommandProfile());
|
||||||
let worldRef: InMemoryTurnWorld | null = null;
|
let worldRef: InMemoryTurnWorld | null = null;
|
||||||
const worldOptions: InMemoryTurnWorldOptions = {
|
const worldOptions: InMemoryTurnWorldOptions = {
|
||||||
schedule,
|
schedule,
|
||||||
generalTurnHandler:
|
generalTurnHandler:
|
||||||
options.generalTurnHandler ??
|
options.generalTurnHandler ??
|
||||||
createReservedTurnHandler({
|
(await createReservedTurnHandler({
|
||||||
reservedTurns: reservedTurnStoreHandle!.store,
|
reservedTurns: reservedTurnStoreHandle!.store,
|
||||||
scenarioConfig: snapshot.scenarioConfig,
|
scenarioConfig: snapshot.scenarioConfig,
|
||||||
scenarioMeta: snapshot.scenarioMeta,
|
scenarioMeta: snapshot.scenarioMeta,
|
||||||
@@ -94,7 +104,8 @@ export const createTurnDaemonRuntime = async (
|
|||||||
map: snapshot.map,
|
map: snapshot.map,
|
||||||
unitSet: snapshot.unitSet,
|
unitSet: snapshot.unitSet,
|
||||||
getWorld: () => worldRef,
|
getWorld: () => worldRef,
|
||||||
}),
|
commandProfile,
|
||||||
|
})),
|
||||||
calendarHandler: options.calendarHandler,
|
calendarHandler: options.calendarHandler,
|
||||||
};
|
};
|
||||||
const world = new InMemoryTurnWorld(resolvedState, snapshot, worldOptions);
|
const world = new InMemoryTurnWorld(resolvedState, snapshot, worldOptions);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export * from './definition.js';
|
export * from './definition.js';
|
||||||
export * from './engine.js';
|
export * from './engine.js';
|
||||||
|
export * from './turn/commandEnv.js';
|
||||||
|
export * from './turn/commandProfile.js';
|
||||||
export * from './turn/general/index.js';
|
export * from './turn/general/index.js';
|
||||||
export * from './turn/nation/index.js';
|
export * from './turn/nation/index.js';
|
||||||
export * from './instant/general/index.js';
|
export * from './instant/general/index.js';
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export interface TurnCommandEnv {
|
||||||
|
develCost: number;
|
||||||
|
trainDelta: number;
|
||||||
|
atmosDelta: number;
|
||||||
|
maxTrainByCommand: number;
|
||||||
|
maxAtmosByCommand: number;
|
||||||
|
sabotageDefaultProb: number;
|
||||||
|
sabotageProbCoefByStat: number;
|
||||||
|
sabotageDefenceCoefByGeneralCount: number;
|
||||||
|
sabotageDamageMin: number;
|
||||||
|
sabotageDamageMax: number;
|
||||||
|
openingPartYear: number;
|
||||||
|
maxGeneral: number;
|
||||||
|
defaultNpcGold: number;
|
||||||
|
defaultNpcRice: number;
|
||||||
|
defaultCrewTypeId: number;
|
||||||
|
defaultSpecialDomestic: string | null;
|
||||||
|
defaultSpecialWar: string | null;
|
||||||
|
initialNationGenLimit: number;
|
||||||
|
maxTechLevel: number;
|
||||||
|
baseGold: number;
|
||||||
|
baseRice: number;
|
||||||
|
maxResourceActionAmount: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
GENERAL_TURN_COMMAND_KEYS,
|
||||||
|
isGeneralTurnCommandKey,
|
||||||
|
type GeneralTurnCommandKey,
|
||||||
|
} from './general/index.js';
|
||||||
|
import {
|
||||||
|
NATION_TURN_COMMAND_KEYS,
|
||||||
|
isNationTurnCommandKey,
|
||||||
|
type NationTurnCommandKey,
|
||||||
|
} from './nation/index.js';
|
||||||
|
|
||||||
|
export interface TurnCommandProfile {
|
||||||
|
general: GeneralTurnCommandKey[];
|
||||||
|
nation: NationTurnCommandKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
|
||||||
|
const asStringArray = (value: unknown): string[] | null => {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const list = value.filter((entry): entry is string => typeof entry === 'string');
|
||||||
|
return list.length > 0 ? list : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseKeyList = <
|
||||||
|
T extends string
|
||||||
|
>(options: {
|
||||||
|
raw: unknown;
|
||||||
|
defaults: T[];
|
||||||
|
isKey: (value: string) => value is T;
|
||||||
|
label: string;
|
||||||
|
}): T[] => {
|
||||||
|
const rawList = asStringArray(options.raw);
|
||||||
|
if (!rawList) {
|
||||||
|
return options.defaults;
|
||||||
|
}
|
||||||
|
const parsed: T[] = [];
|
||||||
|
for (const value of rawList) {
|
||||||
|
if (!options.isKey(value)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unknown ${options.label} command key: ${value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parsed.push(value);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_TURN_COMMAND_PROFILE: TurnCommandProfile = {
|
||||||
|
general: [...GENERAL_TURN_COMMAND_KEYS],
|
||||||
|
nation: [...NATION_TURN_COMMAND_KEYS],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseTurnCommandProfile = (
|
||||||
|
raw: unknown,
|
||||||
|
fallback: TurnCommandProfile = DEFAULT_TURN_COMMAND_PROFILE
|
||||||
|
): TurnCommandProfile => {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
general: parseKeyList({
|
||||||
|
raw: raw.general,
|
||||||
|
defaults: fallback.general,
|
||||||
|
isKey: isGeneralTurnCommandKey,
|
||||||
|
label: 'general',
|
||||||
|
}),
|
||||||
|
nation: parseKeyList({
|
||||||
|
raw: raw.nation,
|
||||||
|
defaults: fallback.nation,
|
||||||
|
isKey: isNationTurnCommandKey,
|
||||||
|
label: 'nation',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface UprisingArgs {}
|
export interface UprisingArgs {}
|
||||||
|
|
||||||
@@ -53,3 +55,11 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_거병',
|
||||||
|
category: '전략',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface FoundingArgs {}
|
export interface FoundingArgs {}
|
||||||
|
|
||||||
@@ -53,3 +55,11 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_건국',
|
||||||
|
category: '전략',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import {
|
|||||||
createNationPatchEffect,
|
createNationPatchEffect,
|
||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface TechResearchArgs {}
|
export interface TechResearchArgs {}
|
||||||
|
|
||||||
@@ -127,3 +129,15 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_기술연구',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({
|
||||||
|
costGold: env.develCost,
|
||||||
|
maxTechLevel: env.maxTechLevel,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -18,3 +20,12 @@ export class ActionDefinition<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_농지개간',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({ develCost: env.develCost }),
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface BoostMoraleArgs {}
|
export interface BoostMoraleArgs {}
|
||||||
|
|
||||||
@@ -89,3 +91,15 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_사기진작',
|
||||||
|
category: '군사',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({
|
||||||
|
atmosDelta: env.atmosDelta,
|
||||||
|
maxAtmosByCommand: env.maxAtmosByCommand,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
createGeneralPatchEffect,
|
createGeneralPatchEffect,
|
||||||
createLogEffect,
|
createLogEffect,
|
||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
|
||||||
|
|
||||||
@@ -437,3 +439,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_상업투자',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition([], env),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -18,3 +20,12 @@ export class ActionDefinition<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_성벽보수',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({ develCost: env.develCost }),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -18,3 +20,12 @@ export class ActionDefinition<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_수비강화',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({ develCost: env.develCost }),
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface RecoveryArgs {}
|
export interface RecoveryArgs {}
|
||||||
|
|
||||||
@@ -77,3 +79,11 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_요양',
|
||||||
|
category: '개인',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import {
|
|||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
import { buildRecruitmentGeneral } from './recruitment.js';
|
import { buildRecruitmentGeneral } from './recruitment.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface VolunteerRecruitArgs {}
|
export interface VolunteerRecruitArgs {}
|
||||||
|
|
||||||
@@ -431,3 +433,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_의병모집',
|
||||||
|
category: '전략',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition([], env),
|
||||||
|
};
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
import { buildRecruitmentGeneral } from './recruitment.js';
|
import { buildRecruitmentGeneral } from './recruitment.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface TalentScoutArgs {}
|
export interface TalentScoutArgs {}
|
||||||
|
|
||||||
@@ -468,3 +470,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_인재탐색',
|
||||||
|
category: '인사',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition([], env),
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createLogEffect } from '../../engine.js';
|
import { createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface AppointmentArgs {
|
export interface AppointmentArgs {
|
||||||
destNationId: number;
|
destNationId: number;
|
||||||
@@ -63,3 +65,11 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_임관',
|
||||||
|
category: '전략',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destNationId: 0 },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -18,3 +20,12 @@ export class ActionDefinition<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_주민선정',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({ develCost: env.develCost }),
|
||||||
|
};
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import {
|
|||||||
createLogEffect,
|
createLogEffect,
|
||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import type { MapDefinition, UnitSetDefinition } from '../../../world/types.js';
|
import type { MapDefinition, UnitSetDefinition } from '../../../world/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
import {
|
import {
|
||||||
type CrewTypeAvailabilityContext,
|
type CrewTypeAvailabilityContext,
|
||||||
findCrewTypeById,
|
findCrewTypeById,
|
||||||
@@ -600,3 +602,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_징병',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: true,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition([], {}),
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createLogEffect } from '../../engine.js';
|
import { createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface DispatchArgs {
|
export interface DispatchArgs {
|
||||||
destCityId: number;
|
destCityId: number;
|
||||||
@@ -69,3 +71,11 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_출병',
|
||||||
|
category: '군사',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destCityId: 0 },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -18,3 +20,12 @@ export class ActionDefinition<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_치안강화',
|
||||||
|
category: '내정',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({ develCost: env.develCost }),
|
||||||
|
};
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import {
|
|||||||
createLogEffect,
|
createLogEffect,
|
||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface FireAttackArgs {
|
export interface FireAttackArgs {
|
||||||
destCityId: number;
|
destCityId: number;
|
||||||
@@ -503,3 +505,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_화계',
|
||||||
|
category: '계략',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destCityId: 0 },
|
||||||
|
createDefinition: (env: TurnCommandEnv) => new ActionDefinition([], env),
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface TrainingArgs {}
|
export interface TrainingArgs {}
|
||||||
|
|
||||||
@@ -89,3 +91,15 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_훈련',
|
||||||
|
category: '군사',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (env: TurnCommandEnv) =>
|
||||||
|
new ActionDefinition({
|
||||||
|
trainDelta: env.trainDelta,
|
||||||
|
maxTrainByCommand: env.maxTrainByCommand,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type * as UprisingModule from './che_거병.js';
|
||||||
|
import type * as AppointmentModule from './che_임관.js';
|
||||||
|
import type * as FoundingModule from './che_건국.js';
|
||||||
|
import type * as TrainingModule from './che_훈련.js';
|
||||||
|
import type * as BoostMoraleModule from './che_사기진작.js';
|
||||||
|
import type * as RecoveryModule from './che_요양.js';
|
||||||
|
import type * as DispatchModule from './che_출병.js';
|
||||||
|
import type * as ResidentsSelectionModule from './che_주민선정.js';
|
||||||
|
import type * as FarmingModule from './che_농지개간.js';
|
||||||
|
import type * as CommerceInvestmentModule from './che_상업투자.js';
|
||||||
|
import type * as TechResearchModule from './che_기술연구.js';
|
||||||
|
import type * as SecurityUpgradeModule from './che_치안강화.js';
|
||||||
|
import type * as DefenceUpgradeModule from './che_수비강화.js';
|
||||||
|
import type * as WallRepairModule from './che_성벽보수.js';
|
||||||
|
import type * as FireAttackModule from './che_화계.js';
|
||||||
|
import type * as TalentScoutModule from './che_인재탐색.js';
|
||||||
|
import type * as VolunteerRecruitModule from './che_의병모집.js';
|
||||||
|
import type * as RecruitModule from './che_징병.js';
|
||||||
|
import type * as RestModule from './휴식.js';
|
||||||
|
|
||||||
export type GeneralTurnCommandKey =
|
export type GeneralTurnCommandKey =
|
||||||
| 'che_거병'
|
| 'che_거병'
|
||||||
| 'che_임관'
|
| 'che_임관'
|
||||||
@@ -19,25 +41,40 @@ export type GeneralTurnCommandKey =
|
|||||||
| 'che_징병'
|
| 'che_징병'
|
||||||
| '휴식';
|
| '휴식';
|
||||||
|
|
||||||
import type * as UprisingModule from './che_거병.js';
|
export const GENERAL_TURN_COMMAND_KEYS: GeneralTurnCommandKey[] = [
|
||||||
import type * as AppointmentModule from './che_임관.js';
|
'che_거병',
|
||||||
import type * as FoundingModule from './che_건국.js';
|
'che_임관',
|
||||||
import type * as TrainingModule from './che_훈련.js';
|
'che_건국',
|
||||||
import type * as BoostMoraleModule from './che_사기진작.js';
|
'che_훈련',
|
||||||
import type * as RecoveryModule from './che_요양.js';
|
'che_사기진작',
|
||||||
import type * as DispatchModule from './che_출병.js';
|
'che_요양',
|
||||||
import type * as ResidentsSelectionModule from './che_주민선정.js';
|
'che_출병',
|
||||||
import type * as FarmingModule from './che_농지개간.js';
|
'che_주민선정',
|
||||||
import type * as CommerceInvestmentModule from './che_상업투자.js';
|
'che_농지개간',
|
||||||
import type * as TechResearchModule from './che_기술연구.js';
|
'che_상업투자',
|
||||||
import type * as SecurityUpgradeModule from './che_치안강화.js';
|
'che_기술연구',
|
||||||
import type * as DefenceUpgradeModule from './che_수비강화.js';
|
'che_치안강화',
|
||||||
import type * as WallRepairModule from './che_성벽보수.js';
|
'che_수비강화',
|
||||||
import type * as FireAttackModule from './che_화계.js';
|
'che_성벽보수',
|
||||||
import type * as TalentScoutModule from './che_인재탐색.js';
|
'che_화계',
|
||||||
import type * as VolunteerRecruitModule from './che_의병모집.js';
|
'che_인재탐색',
|
||||||
import type * as RecruitModule from './che_징병.js';
|
'che_의병모집',
|
||||||
import type * as RestModule from './휴식.js';
|
'che_징병',
|
||||||
|
'휴식',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const isGeneralTurnCommandKey = (
|
||||||
|
value: string
|
||||||
|
): value is GeneralTurnCommandKey =>
|
||||||
|
(GENERAL_TURN_COMMAND_KEYS as string[]).includes(value);
|
||||||
|
|
||||||
|
export interface GeneralTurnCommandSpec {
|
||||||
|
key: GeneralTurnCommandKey;
|
||||||
|
category: string;
|
||||||
|
reqArg: boolean;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
createDefinition(env: TurnCommandEnv): GeneralActionDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
export type GeneralTurnCommandModule =
|
export type GeneralTurnCommandModule =
|
||||||
| typeof UprisingModule
|
| typeof UprisingModule
|
||||||
@@ -106,6 +143,26 @@ export class GeneralTurnCommandLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadGeneralTurnCommandSpecs = async (
|
||||||
|
keys: GeneralTurnCommandKey[],
|
||||||
|
loader: GeneralTurnCommandLoader = new GeneralTurnCommandLoader()
|
||||||
|
): Promise<GeneralTurnCommandSpec[]> => {
|
||||||
|
const specs: GeneralTurnCommandSpec[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const key of keys) {
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
const module = await loader.load(key);
|
||||||
|
if (!('commandSpec' in module)) {
|
||||||
|
throw new Error(`Missing commandSpec for general command: ${key}`);
|
||||||
|
}
|
||||||
|
specs.push(module.commandSpec);
|
||||||
|
}
|
||||||
|
return specs;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ActionDefinition as UprisingActionDefinition,
|
ActionDefinition as UprisingActionDefinition,
|
||||||
} from './che_거병.js';
|
} from './che_거병.js';
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createLogEffect } from '../../engine.js';
|
import { createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface RestArgs {}
|
export interface RestArgs {}
|
||||||
|
|
||||||
@@ -66,3 +68,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: '휴식',
|
||||||
|
category: '개인',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import type {
|
|||||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface AssignmentArgs {
|
export interface AssignmentArgs {
|
||||||
destGeneralId: number;
|
destGeneralId: number;
|
||||||
@@ -206,3 +208,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_발령',
|
||||||
|
category: '인사',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destGeneralId: 0, destCityId: 0 },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition({}),
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { createLogEffect } from '../../engine.js';
|
import { createLogEffect } from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface DeclareWarArgs {
|
export interface DeclareWarArgs {
|
||||||
destNationId: number;
|
destNationId: number;
|
||||||
@@ -72,3 +74,11 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_선전포고',
|
||||||
|
category: '외교',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destNationId: 0 },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
createNationPatchEffect,
|
createNationPatchEffect,
|
||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
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 { JosaUtil } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface AwardArgs {
|
export interface AwardArgs {
|
||||||
@@ -266,3 +268,21 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_포상',
|
||||||
|
category: '인사',
|
||||||
|
reqArg: true,
|
||||||
|
args: { isGold: true, amount: 1, destGeneralId: 0 },
|
||||||
|
createDefinition: (env: TurnCommandEnv) => {
|
||||||
|
const maxAmount =
|
||||||
|
env.maxResourceActionAmount > 0
|
||||||
|
? env.maxResourceActionAmount
|
||||||
|
: Math.max(env.baseGold, env.baseRice, 1000);
|
||||||
|
return new ActionDefinition({
|
||||||
|
baseGold: env.baseGold,
|
||||||
|
baseRice: env.baseRice,
|
||||||
|
maxAmount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,35 @@
|
|||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type * as NationRestModule from './휴식.js';
|
||||||
|
import type * as AwardModule from './che_포상.js';
|
||||||
|
import type * as AssignmentModule from './che_발령.js';
|
||||||
|
import type * as DeclarationModule from './che_선전포고.js';
|
||||||
|
|
||||||
export type NationTurnCommandKey =
|
export type NationTurnCommandKey =
|
||||||
| '휴식'
|
| '휴식'
|
||||||
| 'che_포상'
|
| 'che_포상'
|
||||||
| 'che_발령'
|
| 'che_발령'
|
||||||
| 'che_선전포고';
|
| 'che_선전포고';
|
||||||
|
|
||||||
import type * as NationRestModule from './휴식.js';
|
export const NATION_TURN_COMMAND_KEYS: NationTurnCommandKey[] = [
|
||||||
import type * as AwardModule from './che_포상.js';
|
'휴식',
|
||||||
import type * as AssignmentModule from './che_발령.js';
|
'che_포상',
|
||||||
import type * as DeclarationModule from './che_선전포고.js';
|
'che_발령',
|
||||||
|
'che_선전포고',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const isNationTurnCommandKey = (
|
||||||
|
value: string
|
||||||
|
): value is NationTurnCommandKey =>
|
||||||
|
(NATION_TURN_COMMAND_KEYS as string[]).includes(value);
|
||||||
|
|
||||||
|
export interface NationTurnCommandSpec {
|
||||||
|
key: NationTurnCommandKey;
|
||||||
|
category: string;
|
||||||
|
reqArg: boolean;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
createDefinition(env: TurnCommandEnv): GeneralActionDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
export type NationTurnCommandModule =
|
export type NationTurnCommandModule =
|
||||||
| typeof NationRestModule
|
| typeof NationRestModule
|
||||||
@@ -46,6 +68,26 @@ export class NationTurnCommandLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadNationTurnCommandSpecs = async (
|
||||||
|
keys: NationTurnCommandKey[],
|
||||||
|
loader: NationTurnCommandLoader = new NationTurnCommandLoader()
|
||||||
|
): Promise<NationTurnCommandSpec[]> => {
|
||||||
|
const specs: NationTurnCommandSpec[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const key of keys) {
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
const module = await loader.load(key);
|
||||||
|
if (!('commandSpec' in module)) {
|
||||||
|
throw new Error(`Missing commandSpec for nation command: ${key}`);
|
||||||
|
}
|
||||||
|
specs.push(module.commandSpec);
|
||||||
|
}
|
||||||
|
return specs;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ActionDefinition as NationRestActionDefinition,
|
ActionDefinition as NationRestActionDefinition,
|
||||||
ActionResolver as NationRestActionResolver,
|
ActionResolver as NationRestActionResolver,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type {
|
|||||||
GeneralActionOutcome,
|
GeneralActionOutcome,
|
||||||
GeneralActionResolveContext,
|
GeneralActionResolveContext,
|
||||||
} from '../../engine.js';
|
} from '../../engine.js';
|
||||||
|
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||||
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface NationRestArgs {}
|
export interface NationRestArgs {}
|
||||||
|
|
||||||
@@ -56,3 +58,11 @@ export class ActionDefinition<
|
|||||||
return this.resolver.resolve(context, args);
|
return this.resolver.resolve(context, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: '휴식',
|
||||||
|
category: '휴식',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"general": [
|
||||||
|
"che_거병",
|
||||||
|
"che_임관",
|
||||||
|
"che_건국",
|
||||||
|
"che_훈련",
|
||||||
|
"che_사기진작",
|
||||||
|
"che_요양",
|
||||||
|
"che_출병",
|
||||||
|
"che_주민선정",
|
||||||
|
"che_농지개간",
|
||||||
|
"che_상업투자",
|
||||||
|
"che_기술연구",
|
||||||
|
"che_치안강화",
|
||||||
|
"che_수비강화",
|
||||||
|
"che_성벽보수",
|
||||||
|
"che_화계",
|
||||||
|
"che_인재탐색",
|
||||||
|
"che_의병모집",
|
||||||
|
"che_징병",
|
||||||
|
"휴식"
|
||||||
|
],
|
||||||
|
"nation": [
|
||||||
|
"휴식",
|
||||||
|
"che_포상",
|
||||||
|
"che_발령",
|
||||||
|
"che_선전포고"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user