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,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
AppointmentActionDefinition,
|
||||
AssignmentActionDefinition,
|
||||
BoostMoraleActionDefinition,
|
||||
AwardActionDefinition,
|
||||
CommerceInvestmentActionDefinition,
|
||||
DeclarationActionDefinition,
|
||||
DefenceUpgradeActionDefinition,
|
||||
DispatchActionDefinition,
|
||||
evaluateConstraints,
|
||||
FireAttackActionDefinition,
|
||||
FarmingActionDefinition,
|
||||
FoundingActionDefinition,
|
||||
NationRestActionDefinition,
|
||||
RecoveryActionDefinition,
|
||||
ResidentsSelectionActionDefinition,
|
||||
RecruitActionDefinition,
|
||||
RestActionDefinition,
|
||||
SecurityUpgradeActionDefinition,
|
||||
TechResearchActionDefinition,
|
||||
TalentScoutActionDefinition,
|
||||
TrainingActionDefinition,
|
||||
UprisingActionDefinition,
|
||||
VolunteerRecruitActionDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
@@ -52,6 +65,10 @@ export interface TurnCommandTable {
|
||||
|
||||
interface CommandEnv {
|
||||
develCost: number;
|
||||
trainDelta: number;
|
||||
atmosDelta: number;
|
||||
maxTrainByCommand: number;
|
||||
maxAtmosByCommand: number;
|
||||
sabotageDefaultProb: number;
|
||||
sabotageProbCoefByStat: number;
|
||||
sabotageDefenceCoefByGeneralCount: number;
|
||||
@@ -65,6 +82,7 @@ interface CommandEnv {
|
||||
defaultSpecialDomestic: string | null;
|
||||
defaultSpecialWar: string | null;
|
||||
initialNationGenLimit: number;
|
||||
maxTechLevel: number;
|
||||
baseGold: number;
|
||||
baseRice: number;
|
||||
maxResourceActionAmount: number;
|
||||
@@ -197,6 +215,14 @@ const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
||||
['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'],
|
||||
@@ -260,6 +286,7 @@ const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
||||
['initialNationGenLimit'],
|
||||
0
|
||||
),
|
||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||
maxResourceActionAmount: resolveNumber(
|
||||
@@ -467,6 +494,70 @@ const buildEntries = (env: CommandEnv): {
|
||||
reqArg: false,
|
||||
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, {
|
||||
@@ -475,6 +566,39 @@ const buildEntries = (env: CommandEnv): {
|
||||
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, {}),
|
||||
@@ -539,6 +663,12 @@ const buildEntries = (env: CommandEnv): {
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '외교',
|
||||
definition: new DeclarationActionDefinition(),
|
||||
reqArg: true,
|
||||
args: { destNationId: 0 },
|
||||
},
|
||||
{
|
||||
category: '인사',
|
||||
definition: assignmentDefinition,
|
||||
|
||||
@@ -13,16 +13,29 @@ import type {
|
||||
UnitSetDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
AppointmentActionDefinition,
|
||||
AssignmentActionDefinition,
|
||||
BoostMoraleActionDefinition,
|
||||
AwardActionDefinition,
|
||||
CommerceInvestmentActionDefinition,
|
||||
DeclarationActionDefinition,
|
||||
DefenceUpgradeActionDefinition,
|
||||
DispatchActionDefinition,
|
||||
evaluateConstraints,
|
||||
FireAttackActionDefinition,
|
||||
FarmingActionDefinition,
|
||||
FoundingActionDefinition,
|
||||
NationRestActionDefinition,
|
||||
RecoveryActionDefinition,
|
||||
ResidentsSelectionActionDefinition,
|
||||
RecruitActionDefinition,
|
||||
resolveGeneralAction,
|
||||
RestActionDefinition,
|
||||
SecurityUpgradeActionDefinition,
|
||||
TechResearchActionDefinition,
|
||||
TalentScoutActionDefinition,
|
||||
TrainingActionDefinition,
|
||||
UprisingActionDefinition,
|
||||
VolunteerRecruitActionDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||
@@ -38,6 +51,10 @@ import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
||||
|
||||
interface CommandEnv {
|
||||
develCost: number;
|
||||
trainDelta: number;
|
||||
atmosDelta: number;
|
||||
maxTrainByCommand: number;
|
||||
maxAtmosByCommand: number;
|
||||
sabotageDefaultProb: number;
|
||||
sabotageProbCoefByStat: number;
|
||||
sabotageDefenceCoefByGeneralCount: number;
|
||||
@@ -51,6 +68,7 @@ interface CommandEnv {
|
||||
defaultSpecialDomestic: string | null;
|
||||
defaultSpecialWar: string | null;
|
||||
initialNationGenLimit: number;
|
||||
maxTechLevel: number;
|
||||
baseGold: number;
|
||||
baseRice: number;
|
||||
maxResourceActionAmount: number;
|
||||
@@ -125,6 +143,14 @@ const buildCommandEnv = (
|
||||
['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'],
|
||||
@@ -188,6 +214,7 @@ const buildCommandEnv = (
|
||||
['initialNationGenLimit'],
|
||||
0
|
||||
),
|
||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||
maxResourceActionAmount: resolveNumber(
|
||||
@@ -421,10 +448,56 @@ 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));
|
||||
@@ -442,6 +515,7 @@ const buildNationDefinitions = (
|
||||
? env.maxResourceActionAmount
|
||||
: Math.max(env.baseGold, env.baseRice, 1000);
|
||||
definitions.set('휴식', new NationRestActionDefinition());
|
||||
definitions.set('che_선전포고', new DeclarationActionDefinition());
|
||||
definitions.set(
|
||||
'che_포상',
|
||||
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 =
|
||||
| '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 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';
|
||||
@@ -14,7 +40,20 @@ import type * as RecruitModule from './che_징병.js';
|
||||
import type * as RestModule from './휴식.js';
|
||||
|
||||
export type GeneralTurnCommandModule =
|
||||
| typeof UprisingModule
|
||||
| typeof AppointmentModule
|
||||
| typeof FoundingModule
|
||||
| typeof TrainingModule
|
||||
| typeof BoostMoraleModule
|
||||
| typeof RecoveryModule
|
||||
| typeof DispatchModule
|
||||
| typeof ResidentsSelectionModule
|
||||
| typeof FarmingModule
|
||||
| typeof CommerceInvestmentModule
|
||||
| typeof TechResearchModule
|
||||
| typeof SecurityUpgradeModule
|
||||
| typeof DefenceUpgradeModule
|
||||
| typeof WallRepairModule
|
||||
| typeof FireAttackModule
|
||||
| typeof TalentScoutModule
|
||||
| typeof VolunteerRecruitModule
|
||||
@@ -27,7 +66,20 @@ const defaultImporters: Record<
|
||||
GeneralTurnCommandKey,
|
||||
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'),
|
||||
@@ -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 {
|
||||
ActionDefinition as CommerceInvestmentActionDefinition,
|
||||
ActionResolver as CommerceInvestmentActionResolver,
|
||||
CommandResolver as CommerceInvestmentCommandResolver,
|
||||
} 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 {
|
||||
ActionDefinition as FireAttackActionDefinition,
|
||||
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 AwardModule from './che_포상.js';
|
||||
import type * as AssignmentModule from './che_발령.js';
|
||||
import type * as DeclarationModule from './che_선전포고.js';
|
||||
|
||||
export type NationTurnCommandModule =
|
||||
| typeof NationRestModule
|
||||
| typeof AwardModule
|
||||
| typeof AssignmentModule;
|
||||
| typeof AssignmentModule
|
||||
| typeof DeclarationModule;
|
||||
|
||||
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
|
||||
|
||||
@@ -18,6 +24,7 @@ const defaultImporters: Record<
|
||||
휴식: async () => import('./휴식.js'),
|
||||
che_포상: async () => import('./che_포상.js'),
|
||||
che_발령: async () => import('./che_발령.js'),
|
||||
che_선전포고: async () => import('./che_선전포고.js'),
|
||||
};
|
||||
|
||||
export class NationTurnCommandLoader {
|
||||
@@ -52,3 +59,6 @@ export {
|
||||
ActionDefinition as AssignmentActionDefinition,
|
||||
ActionResolver as AssignmentActionResolver,
|
||||
} 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 => ({
|
||||
name: 'AlwaysFail',
|
||||
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 = (
|
||||
key: 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 => ({
|
||||
name: 'ExistsDestGeneral',
|
||||
requires: (ctx) =>
|
||||
|
||||
Reference in New Issue
Block a user