feat: add special action modules for domestic and war triggers

- Implemented '발명' and '인덕' domestic special action modules with modifiers for technology research and population management.
- Added '의술' and '징병' war special action modules to enhance healing and recruitment capabilities during battles.
- Created a general trigger for city healing in '의술' and integrated it with the war action pipeline.
- Developed a comprehensive registry and loader for special action modules to streamline their usage in the game logic.
- Enhanced test coverage for special action modules, ensuring correct application of modifiers and trigger functionalities.
This commit is contained in:
2026-01-03 08:01:04 +00:00
parent eb361e1bed
commit 64953e39a5
29 changed files with 1250 additions and 28 deletions
+1
View File
@@ -16,6 +16,7 @@ export interface WarActionContext<TriggerState extends GeneralTriggerState = Gen
city?: City;
log?: ActionLogger;
rng?: RandUtil;
unit?: WarUnit<TriggerState>;
}
export interface WarActionModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
+7 -7
View File
@@ -14,10 +14,6 @@ import { LogFormat } from '../logging/types.js';
import { buildCrewTypeIndex as buildCrewTypeDefinitionIndex } from '../world/unitSet.js';
import { WarActionPipeline } from './actions.js';
import { WarCrewType } from './crewType.js';
import {
ChePilsalActivateTrigger,
ChePilsalAttemptTrigger,
} from './triggersChePilsal.js';
import {
WarTriggerCaller,
createWarTriggerEnv,
@@ -84,7 +80,12 @@ const appendCrewTypeTriggers = (
continue;
}
const trigger = factory(unit);
if (trigger) {
if (!trigger) {
continue;
}
if (trigger instanceof WarTriggerCaller) {
caller.merge(trigger);
} else {
caller.append(trigger);
}
}
@@ -109,8 +110,7 @@ const buildBattlePhaseTriggers = (
): WarTriggerCaller => {
const caller = new WarTriggerCaller();
if (unit instanceof WarUnitGeneral) {
caller.append(new ChePilsalAttemptTrigger(unit));
caller.append(new ChePilsalActivateTrigger(unit));
appendCrewTypeTriggers(caller, unit, ['che_필살'], registry);
const context = unit.getActionContext();
caller.merge(unit.getActionPipeline().getBattlePhaseTriggerList(context));
}
+1 -1
View File
@@ -4,6 +4,6 @@ export * from './engine.js';
export * from './aftermath.js';
export * from './units.js';
export * from './triggers.js';
export * from './triggersChePilsal.js';
export * from './triggers/index.js';
export * from './crewType.js';
export * from './utils.js';
+3 -1
View File
@@ -24,7 +24,9 @@ export const createWarTriggerEnv = (): WarTriggerEnv => ({
export class WarTriggerCaller extends TriggerCaller<WarTriggerContext, WarTriggerEnv> {}
export type WarTriggerFactory = (unit: WarUnit) => WarTrigger | null;
export type WarTriggerFactory = (
unit: WarUnit
) => WarTrigger | WarTriggerCaller | null;
export type WarTriggerRegistry = Record<string, WarTriggerFactory>;
@@ -0,0 +1,87 @@
import { LogFormat } from '../../logging/types.js';
import { TriggerPriority } from '../../triggers/core.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '../triggers.js';
import { WarUnitGeneral, type WarUnit } from '../units.js';
import type { WarTriggerModule } from './types.js';
// 의술: 치료 시도
class AttemptTrigger extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Pre + 350);
}
protected actionWar(
self: WarUnit,
_oppose: WarUnit,
_selfEnv: Record<string, unknown>,
_opposeEnv: Record<string, unknown>
): boolean {
if (!(self instanceof WarUnitGeneral)) {
return true;
}
if (self.hasActivatedSkill('치료')) {
return true;
}
if (self.hasActivatedSkill('치료불가')) {
return true;
}
if (!self.rng.nextBool(0.4)) {
return true;
}
self.activateSkill('치료');
return true;
}
}
// 의술: 치료 발동
class ActivateTrigger extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Post + 550);
}
protected actionWar(
self: WarUnit,
oppose: WarUnit,
selfEnv: Record<string, unknown>,
_opposeEnv: Record<string, unknown>
): boolean {
if (!self.hasActivatedSkill('치료')) {
return true;
}
if (selfEnv['치료발동']) {
return true;
}
selfEnv['치료발동'] = true;
oppose
.getLogger()
.pushGeneralBattleDetailLog(
'상대가 <R>치료</>했다!',
LogFormat.PLAIN
);
self
.getLogger()
.pushGeneralBattleDetailLog('<C>치료</>했다!', LogFormat.PLAIN);
oppose.multiplyWarPowerMultiply(0.7);
if (self instanceof WarUnitGeneral) {
self.getGeneral().injury = 0;
}
this.processConsumableItem();
return true;
}
}
export const triggerModule: WarTriggerModule = {
key: 'che_의술',
name: '의술',
info: '[전투] 페이즈마다 치료 발동(아군 피해 30% 감소, 부상 회복)',
createTriggerList: (unit) =>
new WarTriggerCaller(
new AttemptTrigger(unit),
new ActivateTrigger(unit)
),
};
@@ -1,10 +1,11 @@
import { LogFormat } from '../logging/types.js';
import { TriggerPriority } from '../triggers/core.js';
import { BaseWarUnitTrigger } from './triggers.js';
import { WarUnitGeneral, type WarUnit } from './units.js';
import { LogFormat } from '../../logging/types.js';
import { TriggerPriority } from '../../triggers/core.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '../triggers.js';
import { WarUnitGeneral, type WarUnit } from '../units.js';
import type { WarTriggerModule } from './types.js';
// 기본 필살: 시도 단계
export class ChePilsalAttemptTrigger extends BaseWarUnitTrigger {
class AttemptTrigger extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Pre + 120);
}
@@ -33,7 +34,7 @@ export class ChePilsalAttemptTrigger extends BaseWarUnitTrigger {
}
// 기본 필살: 발동 단계
export class ChePilsalActivateTrigger extends BaseWarUnitTrigger {
class ActivateTrigger extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Post + 400);
}
@@ -63,3 +64,14 @@ export class ChePilsalActivateTrigger extends BaseWarUnitTrigger {
return true;
}
}
export const triggerModule: WarTriggerModule = {
key: 'che_필살',
name: '필살',
info: '[전투] 페이즈마다 확률로 필살 발동',
createTriggerList: (unit) =>
new WarTriggerCaller(
new AttemptTrigger(unit),
new ActivateTrigger(unit)
),
};
+81
View File
@@ -0,0 +1,81 @@
import type { WarTriggerModule, WarTriggerModuleExport } from './types.js';
import type { WarTriggerRegistry } from '../triggers.js';
export const WAR_TRIGGER_KEYS = [
'che_필살',
'che_의술',
] as const;
export type WarTriggerKey =
(typeof WAR_TRIGGER_KEYS)[number];
export type WarTriggerImporter = () => Promise<WarTriggerModuleExport>;
const defaultImporters: Record<WarTriggerKey, WarTriggerImporter> = {
che_필살: async () => import('./che_필살.js'),
che_의술: async () => import('./che_의술.js'),
};
export const isWarTriggerKey = (value: string): value is WarTriggerKey =>
WAR_TRIGGER_KEYS.includes(value as WarTriggerKey);
export class WarTriggerLoader {
private readonly cache = new Map<WarTriggerKey, Promise<WarTriggerModule>>();
constructor(
private readonly importers: Record<WarTriggerKey, WarTriggerImporter> = defaultImporters
) {}
async load(key: WarTriggerKey): Promise<WarTriggerModule> {
const cached = this.cache.get(key);
if (cached) {
return cached;
}
const importer = this.importers[key];
if (!importer) {
throw new Error(`Unknown war trigger key: ${key}`);
}
const loading = importer().then((module) => {
if (!('triggerModule' in module)) {
throw new Error(`Missing triggerModule for war trigger: ${key}`);
}
const resolved = module.triggerModule;
if (resolved.key !== key) {
throw new Error(
`War trigger key mismatch: expected ${key}, got ${resolved.key}`
);
}
return resolved;
});
this.cache.set(key, loading);
return loading;
}
}
export const loadWarTriggerModules = async (
keys: WarTriggerKey[],
loader: WarTriggerLoader = new WarTriggerLoader()
): Promise<WarTriggerModule[]> => {
const modules: WarTriggerModule[] = [];
const seen = new Set<string>();
for (const key of keys) {
if (seen.has(key)) {
continue;
}
seen.add(key);
modules.push(await loader.load(key));
}
return modules;
};
export const createWarTriggerRegistry = (
modules: WarTriggerModule[]
): WarTriggerRegistry => {
const registry: WarTriggerRegistry = {};
for (const module of modules) {
registry[module.key] = (unit) => module.createTriggerList(unit);
}
return registry;
};
export type { WarTriggerModule, WarTriggerModuleExport } from './types.js';
+13
View File
@@ -0,0 +1,13 @@
import type { WarTriggerCaller } from '../triggers.js';
import type { WarUnit } from '../units.js';
export interface WarTriggerModule {
key: string;
name: string;
info: string;
createTriggerList(unit: WarUnit): WarTriggerCaller | null;
}
export interface WarTriggerModuleExport {
triggerModule: WarTriggerModule;
}
+1
View File
@@ -435,6 +435,7 @@ export class WarUnitGeneral<
city: this.city,
log: this.logger,
rng: this.rng,
unit: this,
};
}