feat: Add new general actions and constraints for game mechanics
- Introduced new general turn commands: 'che_거병', 'che_임관', 'che_건국', 'che_훈련', 'che_사기진작', 'che_요양', 'che_출병', 'che_주민선정', 'che_농지개간', 'che_기술연구', 'che_치안강화', 'che_수비강화', 'che_성벽보수'. - Implemented action definitions for each new command, including constraints and resolution logic. - Added new constraints: 'beNeutral', 'remainCityCapacityByMax', 'existsDestNation'. - Enhanced city development actions for '농지개간', '성벽보수', and '수비강화'. - Created a new action for declaring war: 'che_선전포고' with appropriate constraints. - Updated existing action definitions to accommodate new game mechanics and ensure proper functionality.
This commit is contained in:
@@ -10,15 +10,28 @@ import type {
|
|||||||
TriggerValue,
|
TriggerValue,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import {
|
import {
|
||||||
|
AppointmentActionDefinition,
|
||||||
AssignmentActionDefinition,
|
AssignmentActionDefinition,
|
||||||
|
BoostMoraleActionDefinition,
|
||||||
AwardActionDefinition,
|
AwardActionDefinition,
|
||||||
CommerceInvestmentActionDefinition,
|
CommerceInvestmentActionDefinition,
|
||||||
|
DeclarationActionDefinition,
|
||||||
|
DefenceUpgradeActionDefinition,
|
||||||
|
DispatchActionDefinition,
|
||||||
evaluateConstraints,
|
evaluateConstraints,
|
||||||
FireAttackActionDefinition,
|
FireAttackActionDefinition,
|
||||||
|
FarmingActionDefinition,
|
||||||
|
FoundingActionDefinition,
|
||||||
NationRestActionDefinition,
|
NationRestActionDefinition,
|
||||||
|
RecoveryActionDefinition,
|
||||||
|
ResidentsSelectionActionDefinition,
|
||||||
RecruitActionDefinition,
|
RecruitActionDefinition,
|
||||||
RestActionDefinition,
|
RestActionDefinition,
|
||||||
|
SecurityUpgradeActionDefinition,
|
||||||
|
TechResearchActionDefinition,
|
||||||
TalentScoutActionDefinition,
|
TalentScoutActionDefinition,
|
||||||
|
TrainingActionDefinition,
|
||||||
|
UprisingActionDefinition,
|
||||||
VolunteerRecruitActionDefinition,
|
VolunteerRecruitActionDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
|
|
||||||
@@ -52,6 +65,10 @@ export interface TurnCommandTable {
|
|||||||
|
|
||||||
interface CommandEnv {
|
interface CommandEnv {
|
||||||
develCost: number;
|
develCost: number;
|
||||||
|
trainDelta: number;
|
||||||
|
atmosDelta: number;
|
||||||
|
maxTrainByCommand: number;
|
||||||
|
maxAtmosByCommand: number;
|
||||||
sabotageDefaultProb: number;
|
sabotageDefaultProb: number;
|
||||||
sabotageProbCoefByStat: number;
|
sabotageProbCoefByStat: number;
|
||||||
sabotageDefenceCoefByGeneralCount: number;
|
sabotageDefenceCoefByGeneralCount: number;
|
||||||
@@ -65,6 +82,7 @@ interface CommandEnv {
|
|||||||
defaultSpecialDomestic: string | null;
|
defaultSpecialDomestic: string | null;
|
||||||
defaultSpecialWar: string | null;
|
defaultSpecialWar: string | null;
|
||||||
initialNationGenLimit: number;
|
initialNationGenLimit: number;
|
||||||
|
maxTechLevel: number;
|
||||||
baseGold: number;
|
baseGold: number;
|
||||||
baseRice: number;
|
baseRice: number;
|
||||||
maxResourceActionAmount: number;
|
maxResourceActionAmount: number;
|
||||||
@@ -197,6 +215,14 @@ const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
|||||||
['develCost', 'develcost', 'develrate'],
|
['develCost', 'develcost', 'develrate'],
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
|
trainDelta: resolveNumber(constValues, ['trainDelta'], 0),
|
||||||
|
atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0),
|
||||||
|
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], 0),
|
||||||
|
maxAtmosByCommand: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['maxAtmosByCommand'],
|
||||||
|
0
|
||||||
|
),
|
||||||
sabotageDefaultProb: resolveNumber(
|
sabotageDefaultProb: resolveNumber(
|
||||||
constValues,
|
constValues,
|
||||||
['sabotageDefaultProb'],
|
['sabotageDefaultProb'],
|
||||||
@@ -260,6 +286,7 @@ const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
|||||||
['initialNationGenLimit'],
|
['initialNationGenLimit'],
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
|
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||||
maxResourceActionAmount: resolveNumber(
|
maxResourceActionAmount: resolveNumber(
|
||||||
@@ -467,6 +494,70 @@ const buildEntries = (env: CommandEnv): {
|
|||||||
reqArg: false,
|
reqArg: false,
|
||||||
args: {},
|
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: '내정',
|
category: '내정',
|
||||||
definition: new CommerceInvestmentActionDefinition(emptyModules, {
|
definition: new CommerceInvestmentActionDefinition(emptyModules, {
|
||||||
@@ -475,6 +566,39 @@ const buildEntries = (env: CommandEnv): {
|
|||||||
reqArg: false,
|
reqArg: false,
|
||||||
args: {},
|
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: '내정',
|
category: '내정',
|
||||||
definition: new RecruitActionDefinition(emptyModules, {}),
|
definition: new RecruitActionDefinition(emptyModules, {}),
|
||||||
@@ -539,6 +663,12 @@ const buildEntries = (env: CommandEnv): {
|
|||||||
reqArg: false,
|
reqArg: false,
|
||||||
args: {},
|
args: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
category: '외교',
|
||||||
|
definition: new DeclarationActionDefinition(),
|
||||||
|
reqArg: true,
|
||||||
|
args: { destNationId: 0 },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
category: '인사',
|
category: '인사',
|
||||||
definition: assignmentDefinition,
|
definition: assignmentDefinition,
|
||||||
|
|||||||
@@ -13,16 +13,29 @@ import type {
|
|||||||
UnitSetDefinition,
|
UnitSetDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import {
|
import {
|
||||||
|
AppointmentActionDefinition,
|
||||||
AssignmentActionDefinition,
|
AssignmentActionDefinition,
|
||||||
|
BoostMoraleActionDefinition,
|
||||||
AwardActionDefinition,
|
AwardActionDefinition,
|
||||||
CommerceInvestmentActionDefinition,
|
CommerceInvestmentActionDefinition,
|
||||||
|
DeclarationActionDefinition,
|
||||||
|
DefenceUpgradeActionDefinition,
|
||||||
|
DispatchActionDefinition,
|
||||||
evaluateConstraints,
|
evaluateConstraints,
|
||||||
FireAttackActionDefinition,
|
FireAttackActionDefinition,
|
||||||
|
FarmingActionDefinition,
|
||||||
|
FoundingActionDefinition,
|
||||||
NationRestActionDefinition,
|
NationRestActionDefinition,
|
||||||
|
RecoveryActionDefinition,
|
||||||
|
ResidentsSelectionActionDefinition,
|
||||||
RecruitActionDefinition,
|
RecruitActionDefinition,
|
||||||
resolveGeneralAction,
|
resolveGeneralAction,
|
||||||
RestActionDefinition,
|
RestActionDefinition,
|
||||||
|
SecurityUpgradeActionDefinition,
|
||||||
|
TechResearchActionDefinition,
|
||||||
TalentScoutActionDefinition,
|
TalentScoutActionDefinition,
|
||||||
|
TrainingActionDefinition,
|
||||||
|
UprisingActionDefinition,
|
||||||
VolunteerRecruitActionDefinition,
|
VolunteerRecruitActionDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||||
@@ -38,6 +51,10 @@ import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
|||||||
|
|
||||||
interface CommandEnv {
|
interface CommandEnv {
|
||||||
develCost: number;
|
develCost: number;
|
||||||
|
trainDelta: number;
|
||||||
|
atmosDelta: number;
|
||||||
|
maxTrainByCommand: number;
|
||||||
|
maxAtmosByCommand: number;
|
||||||
sabotageDefaultProb: number;
|
sabotageDefaultProb: number;
|
||||||
sabotageProbCoefByStat: number;
|
sabotageProbCoefByStat: number;
|
||||||
sabotageDefenceCoefByGeneralCount: number;
|
sabotageDefenceCoefByGeneralCount: number;
|
||||||
@@ -51,6 +68,7 @@ interface CommandEnv {
|
|||||||
defaultSpecialDomestic: string | null;
|
defaultSpecialDomestic: string | null;
|
||||||
defaultSpecialWar: string | null;
|
defaultSpecialWar: string | null;
|
||||||
initialNationGenLimit: number;
|
initialNationGenLimit: number;
|
||||||
|
maxTechLevel: number;
|
||||||
baseGold: number;
|
baseGold: number;
|
||||||
baseRice: number;
|
baseRice: number;
|
||||||
maxResourceActionAmount: number;
|
maxResourceActionAmount: number;
|
||||||
@@ -125,6 +143,14 @@ const buildCommandEnv = (
|
|||||||
['develCost', 'develcost', 'develrate'],
|
['develCost', 'develcost', 'develrate'],
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
|
trainDelta: resolveNumber(constValues, ['trainDelta'], 0),
|
||||||
|
atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0),
|
||||||
|
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], 0),
|
||||||
|
maxAtmosByCommand: resolveNumber(
|
||||||
|
constValues,
|
||||||
|
['maxAtmosByCommand'],
|
||||||
|
0
|
||||||
|
),
|
||||||
sabotageDefaultProb: resolveNumber(
|
sabotageDefaultProb: resolveNumber(
|
||||||
constValues,
|
constValues,
|
||||||
['sabotageDefaultProb'],
|
['sabotageDefaultProb'],
|
||||||
@@ -188,6 +214,7 @@ const buildCommandEnv = (
|
|||||||
['initialNationGenLimit'],
|
['initialNationGenLimit'],
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
|
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||||
maxResourceActionAmount: resolveNumber(
|
maxResourceActionAmount: resolveNumber(
|
||||||
@@ -421,10 +448,56 @@ const buildGeneralDefinitions = (
|
|||||||
env: CommandEnv
|
env: CommandEnv
|
||||||
): Map<string, GeneralActionDefinition> => {
|
): Map<string, GeneralActionDefinition> => {
|
||||||
const definitions = new 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(
|
definitions.set(
|
||||||
'che_상업투자',
|
'che_상업투자',
|
||||||
new CommerceInvestmentActionDefinition([], env)
|
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 FireAttackActionDefinition([], env));
|
||||||
definitions.set('che_인재탐색', new TalentScoutActionDefinition([], env));
|
definitions.set('che_인재탐색', new TalentScoutActionDefinition([], env));
|
||||||
definitions.set('che_의병모집', new VolunteerRecruitActionDefinition([], env));
|
definitions.set('che_의병모집', new VolunteerRecruitActionDefinition([], env));
|
||||||
@@ -442,6 +515,7 @@ const buildNationDefinitions = (
|
|||||||
? env.maxResourceActionAmount
|
? env.maxResourceActionAmount
|
||||||
: Math.max(env.baseGold, env.baseRice, 1000);
|
: Math.max(env.baseGold, env.baseRice, 1000);
|
||||||
definitions.set('휴식', new NationRestActionDefinition());
|
definitions.set('휴식', new NationRestActionDefinition());
|
||||||
|
definitions.set('che_선전포고', new DeclarationActionDefinition());
|
||||||
definitions.set(
|
definitions.set(
|
||||||
'che_포상',
|
'che_포상',
|
||||||
new AwardActionDefinition({
|
new AwardActionDefinition({
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { GeneralTriggerState, TriggerValue } from '../../../domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
|
||||||
|
import { beNeutral } from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface UprisingArgs {}
|
||||||
|
|
||||||
|
const ACTION_NAME = '거병';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, UprisingArgs> {
|
||||||
|
public readonly key = 'che_거병';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): UprisingArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: UprisingArgs): Constraint[] {
|
||||||
|
return [beNeutral()];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: UprisingArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const meta = {
|
||||||
|
...general.meta,
|
||||||
|
uprising: true as TriggerValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createGeneralPatchEffect<TriggerState>(
|
||||||
|
{ meta } as Partial<typeof general>,
|
||||||
|
general.id
|
||||||
|
),
|
||||||
|
createLogEffect(`${ACTION_NAME}을 준비했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { GeneralTriggerState, TriggerValue } from '../../../domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
|
||||||
|
import { beNeutral } from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface FoundingArgs {}
|
||||||
|
|
||||||
|
const ACTION_NAME = '건국';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, FoundingArgs> {
|
||||||
|
public readonly key = 'che_건국';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): FoundingArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: FoundingArgs): Constraint[] {
|
||||||
|
return [beNeutral()];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: FoundingArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const meta = {
|
||||||
|
...general.meta,
|
||||||
|
founding: true as TriggerValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createGeneralPatchEffect<TriggerState>(
|
||||||
|
{ meta } as Partial<typeof general>,
|
||||||
|
general.id
|
||||||
|
),
|
||||||
|
createLogEffect(`${ACTION_NAME}을 준비했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import type {
|
||||||
|
General,
|
||||||
|
GeneralTriggerState,
|
||||||
|
Nation,
|
||||||
|
} from '../../../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
StateView,
|
||||||
|
} from '../../../constraints/types.js';
|
||||||
|
import {
|
||||||
|
notBeNeutral,
|
||||||
|
notWanderingNation,
|
||||||
|
occupiedCity,
|
||||||
|
reqGeneralGold,
|
||||||
|
suppliedCity,
|
||||||
|
} from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import {
|
||||||
|
createGeneralPatchEffect,
|
||||||
|
createLogEffect,
|
||||||
|
createNationPatchEffect,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface TechResearchArgs {}
|
||||||
|
|
||||||
|
export interface TechResearchEnvironment {
|
||||||
|
costGold?: number;
|
||||||
|
techDelta?: number;
|
||||||
|
maxTechLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '기술 연구';
|
||||||
|
const DEFAULT_TECH_DELTA = 1;
|
||||||
|
|
||||||
|
const readTech = (nation: Nation | null | undefined): number => {
|
||||||
|
if (!nation) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const tech = nation.meta.tech;
|
||||||
|
return typeof tech === 'number' ? tech : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, TechResearchArgs> {
|
||||||
|
public readonly key = 'che_기술연구';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
private readonly env: TechResearchEnvironment;
|
||||||
|
|
||||||
|
constructor(env: TechResearchEnvironment = {}) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): TechResearchArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
_ctx: ConstraintContext,
|
||||||
|
_args: TechResearchArgs
|
||||||
|
): Constraint[] {
|
||||||
|
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||||
|
this.env.costGold ?? 0;
|
||||||
|
return [
|
||||||
|
notBeNeutral(),
|
||||||
|
notWanderingNation(),
|
||||||
|
occupiedCity(),
|
||||||
|
suppliedCity(),
|
||||||
|
reqGeneralGold(getRequiredGold),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: TechResearchArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const nation = context.nation ?? null;
|
||||||
|
if (!nation) {
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect('국가 정보를 찾지 못했습니다.', {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = this.env.techDelta ?? DEFAULT_TECH_DELTA;
|
||||||
|
const currentTech = readTech(nation);
|
||||||
|
const maxTech =
|
||||||
|
typeof this.env.maxTechLevel === 'number' &&
|
||||||
|
this.env.maxTechLevel > 0
|
||||||
|
? this.env.maxTechLevel
|
||||||
|
: currentTech + delta;
|
||||||
|
const nextTech = Math.min(currentTech + delta, maxTech);
|
||||||
|
const applied = nextTech - currentTech;
|
||||||
|
const costGold = this.env.costGold ?? 0;
|
||||||
|
const generalPatch: Partial<General<TriggerState>> = {
|
||||||
|
gold: Math.max(0, general.gold - costGold),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createNationPatchEffect(
|
||||||
|
{
|
||||||
|
meta: { ...nation.meta, tech: nextTech },
|
||||||
|
} as Partial<Nation>,
|
||||||
|
nation.id
|
||||||
|
),
|
||||||
|
createGeneralPatchEffect<TriggerState>(generalPatch, general.id),
|
||||||
|
createLogEffect(`${ACTION_NAME}로 기술이 ${applied} 상승했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||||
|
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
key: 'che_농지개간',
|
||||||
|
name: '농지 개간',
|
||||||
|
statKey: 'agriculture',
|
||||||
|
maxKey: 'agricultureMax',
|
||||||
|
label: '농업',
|
||||||
|
baseAmount: 100,
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type {
|
||||||
|
General,
|
||||||
|
GeneralTriggerState,
|
||||||
|
} from '../../../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
StateView,
|
||||||
|
} from '../../../constraints/types.js';
|
||||||
|
import { notBeNeutral, reqGeneralGold } from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface BoostMoraleArgs {}
|
||||||
|
|
||||||
|
export interface BoostMoraleEnvironment {
|
||||||
|
atmosDelta?: number;
|
||||||
|
maxAtmosByCommand?: number;
|
||||||
|
costGold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '사기 진작';
|
||||||
|
const DEFAULT_ATMOS_DELTA = 5;
|
||||||
|
const DEFAULT_MAX_ATMOS = 100;
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number): number =>
|
||||||
|
Math.min(Math.max(value, min), max);
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, BoostMoraleArgs> {
|
||||||
|
public readonly key = 'che_사기진작';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
private readonly env: BoostMoraleEnvironment;
|
||||||
|
|
||||||
|
constructor(env: BoostMoraleEnvironment = {}) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): BoostMoraleArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
_ctx: ConstraintContext,
|
||||||
|
_args: BoostMoraleArgs
|
||||||
|
): Constraint[] {
|
||||||
|
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||||
|
this.env.costGold ?? 0;
|
||||||
|
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: BoostMoraleArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const maxAtmos =
|
||||||
|
this.env.maxAtmosByCommand && this.env.maxAtmosByCommand > 0
|
||||||
|
? this.env.maxAtmosByCommand
|
||||||
|
: DEFAULT_MAX_ATMOS;
|
||||||
|
const delta =
|
||||||
|
this.env.atmosDelta && this.env.atmosDelta > 0
|
||||||
|
? this.env.atmosDelta
|
||||||
|
: DEFAULT_ATMOS_DELTA;
|
||||||
|
const nextAtmos = clamp(general.atmos + delta, 0, maxAtmos);
|
||||||
|
const applied = nextAtmos - general.atmos;
|
||||||
|
const costGold = this.env.costGold ?? 0;
|
||||||
|
const patch: Partial<General<TriggerState>> = {
|
||||||
|
atmos: nextAtmos,
|
||||||
|
gold: Math.max(0, general.gold - costGold),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createGeneralPatchEffect<TriggerState>(patch, general.id),
|
||||||
|
createLogEffect(`${ACTION_NAME}로 사기가 ${applied} 증가했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||||
|
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
key: 'che_성벽보수',
|
||||||
|
name: '성벽 보수',
|
||||||
|
statKey: 'wall',
|
||||||
|
maxKey: 'wallMax',
|
||||||
|
label: '성벽',
|
||||||
|
baseAmount: 50,
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||||
|
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
key: 'che_수비강화',
|
||||||
|
name: '수비 강화',
|
||||||
|
statKey: 'defence',
|
||||||
|
maxKey: 'defenceMax',
|
||||||
|
label: '수비',
|
||||||
|
baseAmount: 50,
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import type {
|
||||||
|
General,
|
||||||
|
GeneralTriggerState,
|
||||||
|
} from '../../../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
StateView,
|
||||||
|
} from '../../../constraints/types.js';
|
||||||
|
import { notBeNeutral, reqGeneralGold } from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface RecoveryArgs {}
|
||||||
|
|
||||||
|
export interface RecoveryEnvironment {
|
||||||
|
injuryDelta?: number;
|
||||||
|
costGold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '요양';
|
||||||
|
const DEFAULT_INJURY_DELTA = 10;
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, RecoveryArgs> {
|
||||||
|
public readonly key = 'che_요양';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
private readonly env: RecoveryEnvironment;
|
||||||
|
|
||||||
|
constructor(env: RecoveryEnvironment = {}) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): RecoveryArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
_ctx: ConstraintContext,
|
||||||
|
_args: RecoveryArgs
|
||||||
|
): Constraint[] {
|
||||||
|
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||||
|
this.env.costGold ?? 0;
|
||||||
|
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: RecoveryArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const delta = this.env.injuryDelta ?? DEFAULT_INJURY_DELTA;
|
||||||
|
const nextInjury = Math.max(0, general.injury - delta);
|
||||||
|
const applied = general.injury - nextInjury;
|
||||||
|
const costGold = this.env.costGold ?? 0;
|
||||||
|
const patch: Partial<General<TriggerState>> = {
|
||||||
|
injury: nextInjury,
|
||||||
|
gold: Math.max(0, general.gold - costGold),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createGeneralPatchEffect<TriggerState>(patch, general.id),
|
||||||
|
createLogEffect(`${ACTION_NAME}으로 부상이 ${applied} 회복되었습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
} from '../../../constraints/types.js';
|
||||||
|
import { beNeutral, existsDestNation } from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface AppointmentArgs {
|
||||||
|
destNationId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '임관';
|
||||||
|
|
||||||
|
const parseNationId = (raw: unknown): number | null => {
|
||||||
|
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw > 0 ? Math.floor(raw) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, AppointmentArgs> {
|
||||||
|
public readonly key = 'che_임관';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): AppointmentArgs | null {
|
||||||
|
const data = raw as { destNationId?: unknown };
|
||||||
|
const destNationId = parseNationId(data?.destNationId);
|
||||||
|
if (destNationId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { destNationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: AppointmentArgs): Constraint[] {
|
||||||
|
return [beNeutral(), existsDestNation()];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
_context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
args: AppointmentArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
void _context;
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect(
|
||||||
|
`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`,
|
||||||
|
{
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||||
|
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
key: 'che_주민선정',
|
||||||
|
name: '주민 선정',
|
||||||
|
statKey: 'population',
|
||||||
|
maxKey: 'populationMax',
|
||||||
|
label: '인구',
|
||||||
|
baseAmount: 1000,
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
|
||||||
|
import {
|
||||||
|
existsDestCity,
|
||||||
|
notBeNeutral,
|
||||||
|
notOccupiedDestCity,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
} from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface DispatchArgs {
|
||||||
|
destCityId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '출병';
|
||||||
|
|
||||||
|
const parseCityId = (raw: unknown): number | null => {
|
||||||
|
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw > 0 ? Math.floor(raw) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, DispatchArgs> {
|
||||||
|
public readonly key = 'che_출병';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): DispatchArgs | null {
|
||||||
|
const data = raw as { destCityId?: unknown };
|
||||||
|
const destCityId = parseCityId(data?.destCityId);
|
||||||
|
if (destCityId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { destCityId };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: DispatchArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
notBeNeutral(),
|
||||||
|
occupiedCity(),
|
||||||
|
suppliedCity(),
|
||||||
|
existsDestCity(),
|
||||||
|
notOccupiedDestCity(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
_context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
args: DispatchArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
void _context;
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect(`${ACTION_NAME}을 준비했습니다. (목표 도시 ${args.destCityId})`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import { CityDevelopmentActionDefinition } from './cityDevelopment.js';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends CityDevelopmentActionDefinition<TriggerState> {
|
||||||
|
constructor(env: { develCost?: number; amount?: number } = {}) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
key: 'che_치안강화',
|
||||||
|
name: '치안 강화',
|
||||||
|
statKey: 'security',
|
||||||
|
maxKey: 'securityMax',
|
||||||
|
label: '치안',
|
||||||
|
baseAmount: 50,
|
||||||
|
},
|
||||||
|
env
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type {
|
||||||
|
General,
|
||||||
|
GeneralTriggerState,
|
||||||
|
} from '../../../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
StateView,
|
||||||
|
} from '../../../constraints/types.js';
|
||||||
|
import { notBeNeutral, reqGeneralGold } from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface TrainingArgs {}
|
||||||
|
|
||||||
|
export interface TrainingEnvironment {
|
||||||
|
trainDelta?: number;
|
||||||
|
maxTrainByCommand?: number;
|
||||||
|
costGold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '훈련';
|
||||||
|
const DEFAULT_TRAIN_DELTA = 5;
|
||||||
|
const DEFAULT_MAX_TRAIN = 100;
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number): number =>
|
||||||
|
Math.min(Math.max(value, min), max);
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, TrainingArgs> {
|
||||||
|
public readonly key = 'che_훈련';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
private readonly env: TrainingEnvironment;
|
||||||
|
|
||||||
|
constructor(env: TrainingEnvironment = {}) {
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): TrainingArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
_ctx: ConstraintContext,
|
||||||
|
_args: TrainingArgs
|
||||||
|
): Constraint[] {
|
||||||
|
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||||
|
this.env.costGold ?? 0;
|
||||||
|
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: TrainingArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const maxTrain =
|
||||||
|
this.env.maxTrainByCommand && this.env.maxTrainByCommand > 0
|
||||||
|
? this.env.maxTrainByCommand
|
||||||
|
: DEFAULT_MAX_TRAIN;
|
||||||
|
const delta =
|
||||||
|
this.env.trainDelta && this.env.trainDelta > 0
|
||||||
|
? this.env.trainDelta
|
||||||
|
: DEFAULT_TRAIN_DELTA;
|
||||||
|
const nextTrain = clamp(general.train + delta, 0, maxTrain);
|
||||||
|
const applied = nextTrain - general.train;
|
||||||
|
const costGold = this.env.costGold ?? 0;
|
||||||
|
const patch: Partial<General<TriggerState>> = {
|
||||||
|
train: nextTrain,
|
||||||
|
gold: Math.max(0, general.gold - costGold),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createGeneralPatchEffect<TriggerState>(patch, general.id),
|
||||||
|
createLogEffect(`${ACTION_NAME}을 통해 훈련도가 ${applied} 증가했습니다.`, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import type {
|
||||||
|
City,
|
||||||
|
General,
|
||||||
|
GeneralTriggerState,
|
||||||
|
} from '../../../domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
StateView,
|
||||||
|
} from '../../../constraints/types.js';
|
||||||
|
import {
|
||||||
|
notBeNeutral,
|
||||||
|
notWanderingNation,
|
||||||
|
occupiedCity,
|
||||||
|
remainCityCapacityByMax,
|
||||||
|
reqGeneralGold,
|
||||||
|
suppliedCity,
|
||||||
|
} from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import {
|
||||||
|
createCityPatchEffect,
|
||||||
|
createGeneralPatchEffect,
|
||||||
|
createLogEffect,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface CityDevelopmentArgs {}
|
||||||
|
|
||||||
|
export interface CityDevelopmentEnvironment {
|
||||||
|
develCost?: number;
|
||||||
|
amount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CityDevelopmentConfig {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
statKey: keyof City;
|
||||||
|
maxKey: keyof City;
|
||||||
|
label: string;
|
||||||
|
baseAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number): number =>
|
||||||
|
Math.min(Math.max(value, min), max);
|
||||||
|
|
||||||
|
const readNumber = (value: unknown): number | null =>
|
||||||
|
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
|
|
||||||
|
export class CityDevelopmentActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, CityDevelopmentArgs> {
|
||||||
|
public readonly key: string;
|
||||||
|
public readonly name: string;
|
||||||
|
private readonly config: CityDevelopmentConfig;
|
||||||
|
private readonly env: CityDevelopmentEnvironment;
|
||||||
|
|
||||||
|
constructor(config: CityDevelopmentConfig, env: CityDevelopmentEnvironment) {
|
||||||
|
this.key = config.key;
|
||||||
|
this.name = config.name;
|
||||||
|
this.config = config;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): CityDevelopmentArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
_ctx: ConstraintContext,
|
||||||
|
_args: CityDevelopmentArgs
|
||||||
|
): Constraint[] {
|
||||||
|
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
|
||||||
|
this.env.develCost ?? 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
notBeNeutral(),
|
||||||
|
notWanderingNation(),
|
||||||
|
occupiedCity(),
|
||||||
|
suppliedCity(),
|
||||||
|
remainCityCapacityByMax(
|
||||||
|
String(this.config.statKey),
|
||||||
|
String(this.config.maxKey),
|
||||||
|
this.config.label
|
||||||
|
),
|
||||||
|
reqGeneralGold(getRequiredGold),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
_args: CityDevelopmentArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const city = context.city;
|
||||||
|
if (!city) {
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect('도시 정보를 찾지 못했습니다.', {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseAmount = this.env.amount ?? this.config.baseAmount;
|
||||||
|
const current = readNumber(city[this.config.statKey]);
|
||||||
|
const max = readNumber(city[this.config.maxKey]);
|
||||||
|
if (current === null || max === null) {
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect('도시 정보를 찾지 못했습니다.', {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValue = clamp(current + baseAmount, 0, max);
|
||||||
|
const costGold = this.env.develCost ?? 0;
|
||||||
|
const generalPatch: Partial<General<TriggerState>> = {
|
||||||
|
gold: Math.max(0, general.gold - costGold),
|
||||||
|
};
|
||||||
|
|
||||||
|
const logMessage = `${this.config.label}이 ${nextValue - current} 증가했습니다.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createCityPatchEffect(
|
||||||
|
{ [this.config.statKey]: nextValue } as Partial<City>,
|
||||||
|
city.id
|
||||||
|
),
|
||||||
|
createGeneralPatchEffect<TriggerState>(generalPatch, general.id),
|
||||||
|
createLogEffect(logMessage, {
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,38 @@
|
|||||||
export type GeneralTurnCommandKey =
|
export type GeneralTurnCommandKey =
|
||||||
|
| 'che_거병'
|
||||||
|
| 'che_임관'
|
||||||
|
| 'che_건국'
|
||||||
|
| 'che_훈련'
|
||||||
|
| 'che_사기진작'
|
||||||
|
| 'che_요양'
|
||||||
|
| 'che_출병'
|
||||||
|
| 'che_주민선정'
|
||||||
|
| 'che_농지개간'
|
||||||
| 'che_상업투자'
|
| 'che_상업투자'
|
||||||
|
| 'che_기술연구'
|
||||||
|
| 'che_치안강화'
|
||||||
|
| 'che_수비강화'
|
||||||
|
| 'che_성벽보수'
|
||||||
| 'che_화계'
|
| 'che_화계'
|
||||||
| 'che_인재탐색'
|
| 'che_인재탐색'
|
||||||
| 'che_의병모집'
|
| 'che_의병모집'
|
||||||
| 'che_징병'
|
| 'che_징병'
|
||||||
| '휴식';
|
| '휴식';
|
||||||
|
|
||||||
|
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 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 FireAttackModule from './che_화계.js';
|
||||||
import type * as TalentScoutModule from './che_인재탐색.js';
|
import type * as TalentScoutModule from './che_인재탐색.js';
|
||||||
import type * as VolunteerRecruitModule from './che_의병모집.js';
|
import type * as VolunteerRecruitModule from './che_의병모집.js';
|
||||||
@@ -14,7 +40,20 @@ import type * as RecruitModule from './che_징병.js';
|
|||||||
import type * as RestModule from './휴식.js';
|
import type * as RestModule from './휴식.js';
|
||||||
|
|
||||||
export type GeneralTurnCommandModule =
|
export type GeneralTurnCommandModule =
|
||||||
|
| typeof UprisingModule
|
||||||
|
| typeof AppointmentModule
|
||||||
|
| typeof FoundingModule
|
||||||
|
| typeof TrainingModule
|
||||||
|
| typeof BoostMoraleModule
|
||||||
|
| typeof RecoveryModule
|
||||||
|
| typeof DispatchModule
|
||||||
|
| typeof ResidentsSelectionModule
|
||||||
|
| typeof FarmingModule
|
||||||
| typeof CommerceInvestmentModule
|
| typeof CommerceInvestmentModule
|
||||||
|
| typeof TechResearchModule
|
||||||
|
| typeof SecurityUpgradeModule
|
||||||
|
| typeof DefenceUpgradeModule
|
||||||
|
| typeof WallRepairModule
|
||||||
| typeof FireAttackModule
|
| typeof FireAttackModule
|
||||||
| typeof TalentScoutModule
|
| typeof TalentScoutModule
|
||||||
| typeof VolunteerRecruitModule
|
| typeof VolunteerRecruitModule
|
||||||
@@ -27,7 +66,20 @@ const defaultImporters: Record<
|
|||||||
GeneralTurnCommandKey,
|
GeneralTurnCommandKey,
|
||||||
GeneralTurnCommandImporter
|
GeneralTurnCommandImporter
|
||||||
> = {
|
> = {
|
||||||
|
che_거병: async () => import('./che_거병.js'),
|
||||||
|
che_임관: async () => import('./che_임관.js'),
|
||||||
|
che_건국: async () => import('./che_건국.js'),
|
||||||
|
che_훈련: async () => import('./che_훈련.js'),
|
||||||
|
che_사기진작: async () => import('./che_사기진작.js'),
|
||||||
|
che_요양: async () => import('./che_요양.js'),
|
||||||
|
che_출병: async () => import('./che_출병.js'),
|
||||||
|
che_주민선정: async () => import('./che_주민선정.js'),
|
||||||
|
che_농지개간: async () => import('./che_농지개간.js'),
|
||||||
che_상업투자: async () => import('./che_상업투자.js'),
|
che_상업투자: async () => import('./che_상업투자.js'),
|
||||||
|
che_기술연구: async () => import('./che_기술연구.js'),
|
||||||
|
che_치안강화: async () => import('./che_치안강화.js'),
|
||||||
|
che_수비강화: async () => import('./che_수비강화.js'),
|
||||||
|
che_성벽보수: async () => import('./che_성벽보수.js'),
|
||||||
che_화계: async () => import('./che_화계.js'),
|
che_화계: async () => import('./che_화계.js'),
|
||||||
che_인재탐색: async () => import('./che_인재탐색.js'),
|
che_인재탐색: async () => import('./che_인재탐색.js'),
|
||||||
che_의병모집: async () => import('./che_의병모집.js'),
|
che_의병모집: async () => import('./che_의병모집.js'),
|
||||||
@@ -54,11 +106,50 @@ export class GeneralTurnCommandLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ActionDefinition as UprisingActionDefinition,
|
||||||
|
} from './che_거병.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as AppointmentActionDefinition,
|
||||||
|
} from './che_임관.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as FoundingActionDefinition,
|
||||||
|
} from './che_건국.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as TrainingActionDefinition,
|
||||||
|
} from './che_훈련.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as BoostMoraleActionDefinition,
|
||||||
|
} from './che_사기진작.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as RecoveryActionDefinition,
|
||||||
|
} from './che_요양.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as DispatchActionDefinition,
|
||||||
|
} from './che_출병.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as ResidentsSelectionActionDefinition,
|
||||||
|
} from './che_주민선정.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as FarmingActionDefinition,
|
||||||
|
} from './che_농지개간.js';
|
||||||
export {
|
export {
|
||||||
ActionDefinition as CommerceInvestmentActionDefinition,
|
ActionDefinition as CommerceInvestmentActionDefinition,
|
||||||
ActionResolver as CommerceInvestmentActionResolver,
|
ActionResolver as CommerceInvestmentActionResolver,
|
||||||
CommandResolver as CommerceInvestmentCommandResolver,
|
CommandResolver as CommerceInvestmentCommandResolver,
|
||||||
} from './che_상업투자.js';
|
} from './che_상업투자.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as TechResearchActionDefinition,
|
||||||
|
} from './che_기술연구.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as SecurityUpgradeActionDefinition,
|
||||||
|
} from './che_치안강화.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as DefenceUpgradeActionDefinition,
|
||||||
|
} from './che_수비강화.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as WallRepairActionDefinition,
|
||||||
|
} from './che_성벽보수.js';
|
||||||
export {
|
export {
|
||||||
ActionDefinition as FireAttackActionDefinition,
|
ActionDefinition as FireAttackActionDefinition,
|
||||||
ActionResolver as FireAttackActionResolver,
|
ActionResolver as FireAttackActionResolver,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||||
|
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
|
||||||
|
import {
|
||||||
|
beChief,
|
||||||
|
existsDestNation,
|
||||||
|
notBeNeutral,
|
||||||
|
occupiedCity,
|
||||||
|
suppliedCity,
|
||||||
|
} from '../../../constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '../../definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} from '../../engine.js';
|
||||||
|
import { createLogEffect } from '../../engine.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||||
|
|
||||||
|
export interface DeclareWarArgs {
|
||||||
|
destNationId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '선전포고';
|
||||||
|
|
||||||
|
const parseNationId = (raw: unknown): number | null => {
|
||||||
|
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw > 0 ? Math.floor(raw) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<TriggerState, DeclareWarArgs> {
|
||||||
|
public readonly key = 'che_선전포고';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): DeclareWarArgs | null {
|
||||||
|
const data = raw as { destNationId?: unknown };
|
||||||
|
const destNationId = parseNationId(data?.destNationId);
|
||||||
|
if (destNationId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { destNationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(_ctx: ConstraintContext, _args: DeclareWarArgs): Constraint[] {
|
||||||
|
return [
|
||||||
|
notBeNeutral(),
|
||||||
|
occupiedCity(),
|
||||||
|
suppliedCity(),
|
||||||
|
beChief(),
|
||||||
|
existsDestNation(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
_context: GeneralActionResolveContext<TriggerState>,
|
||||||
|
args: DeclareWarArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
void _context;
|
||||||
|
return {
|
||||||
|
effects: [
|
||||||
|
createLogEffect(
|
||||||
|
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
|
||||||
|
{
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
export type NationTurnCommandKey = '휴식' | 'che_포상' | 'che_발령';
|
export type NationTurnCommandKey =
|
||||||
|
| '휴식'
|
||||||
|
| 'che_포상'
|
||||||
|
| 'che_발령'
|
||||||
|
| 'che_선전포고';
|
||||||
|
|
||||||
import type * as NationRestModule from './휴식.js';
|
import type * as NationRestModule from './휴식.js';
|
||||||
import type * as AwardModule from './che_포상.js';
|
import type * as AwardModule from './che_포상.js';
|
||||||
import type * as AssignmentModule from './che_발령.js';
|
import type * as AssignmentModule from './che_발령.js';
|
||||||
|
import type * as DeclarationModule from './che_선전포고.js';
|
||||||
|
|
||||||
export type NationTurnCommandModule =
|
export type NationTurnCommandModule =
|
||||||
| typeof NationRestModule
|
| typeof NationRestModule
|
||||||
| typeof AwardModule
|
| typeof AwardModule
|
||||||
| typeof AssignmentModule;
|
| typeof AssignmentModule
|
||||||
|
| typeof DeclarationModule;
|
||||||
|
|
||||||
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
|
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
|
||||||
|
|
||||||
@@ -18,6 +24,7 @@ const defaultImporters: Record<
|
|||||||
휴식: async () => import('./휴식.js'),
|
휴식: async () => import('./휴식.js'),
|
||||||
che_포상: async () => import('./che_포상.js'),
|
che_포상: async () => import('./che_포상.js'),
|
||||||
che_발령: async () => import('./che_발령.js'),
|
che_발령: async () => import('./che_발령.js'),
|
||||||
|
che_선전포고: async () => import('./che_선전포고.js'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class NationTurnCommandLoader {
|
export class NationTurnCommandLoader {
|
||||||
@@ -52,3 +59,6 @@ export {
|
|||||||
ActionDefinition as AssignmentActionDefinition,
|
ActionDefinition as AssignmentActionDefinition,
|
||||||
ActionResolver as AssignmentActionResolver,
|
ActionResolver as AssignmentActionResolver,
|
||||||
} from './che_발령.js';
|
} from './che_발령.js';
|
||||||
|
export {
|
||||||
|
ActionDefinition as DeclarationActionDefinition,
|
||||||
|
} from './che_선전포고.js';
|
||||||
|
|||||||
@@ -174,6 +174,25 @@ export const notBeNeutral = (): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const beNeutral = (): Constraint => ({
|
||||||
|
name: 'BeNeutral',
|
||||||
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(req) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (general.nationId === 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '이미 국가에 속해 있습니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const alwaysFail = (reason: string): Constraint => ({
|
export const alwaysFail = (reason: string): Constraint => ({
|
||||||
name: 'AlwaysFail',
|
name: 'AlwaysFail',
|
||||||
requires: () => [],
|
requires: () => [],
|
||||||
@@ -561,6 +580,36 @@ export const remainCityCapacity = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const remainCityCapacityByMax = (
|
||||||
|
key: string,
|
||||||
|
maxKey: string,
|
||||||
|
label: string
|
||||||
|
): Constraint => ({
|
||||||
|
name: 'RemainCityCapacityByMax',
|
||||||
|
requires: (ctx) =>
|
||||||
|
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const city = readCity(view, ctx.cityId);
|
||||||
|
if (!city) {
|
||||||
|
if (ctx.cityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const record = city as unknown as Record<string, number | undefined>;
|
||||||
|
const current = record[key];
|
||||||
|
const max = record[maxKey];
|
||||||
|
if (current === undefined || max === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (current < max) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: `${label}이 충분합니다.` };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const reqCityCapacity = (
|
export const reqCityCapacity = (
|
||||||
key: string,
|
key: string,
|
||||||
label: string,
|
label: string,
|
||||||
@@ -689,6 +738,34 @@ export const existsDestCity = (): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const existsDestNation = (): Constraint => ({
|
||||||
|
name: 'ExistsDestNation',
|
||||||
|
requires: (ctx) =>
|
||||||
|
resolveDestNationId(ctx) !== undefined
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
kind: 'destNation',
|
||||||
|
id: resolveDestNationId(ctx) ?? 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const destNationId = resolveDestNationId(ctx);
|
||||||
|
if (destNationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'destNation', id: destNationId };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const nation = view.get(req) as Nation | null;
|
||||||
|
if (!nation) {
|
||||||
|
return unknownOrDeny(ctx, [req], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
return allow();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const existsDestGeneral = (): Constraint => ({
|
export const existsDestGeneral = (): Constraint => ({
|
||||||
name: 'ExistsDestGeneral',
|
name: 'ExistsDestGeneral',
|
||||||
requires: (ctx) =>
|
requires: (ctx) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user