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
@@ -0,0 +1,90 @@
import { JosaUtil } from '@sammo-ts/common';
import type { General, GeneralTriggerState } from '../../domain/entities.js';
import { TriggerPriority } from '../core.js';
import {
BaseGeneralTrigger,
type GeneralTriggerContext,
} from '../general.js';
const HEAL_PROBABILITY = 0.5;
const MIN_HEAL_INJURY = 10;
const resolveCityGenerals = <TriggerState extends GeneralTriggerState>(
general: General<TriggerState>,
context: GeneralTriggerContext<TriggerState>
): General<TriggerState>[] => {
const worldView = context.worldView;
if (!worldView) {
return [];
}
const list = worldView.listGeneralsByCity
? worldView.listGeneralsByCity(general.cityId)
: worldView.listGenerals().filter(
(candidate) => candidate.cityId === general.cityId
);
return list.filter((candidate) => candidate.id !== general.id);
};
// 의술 특기의 도시 치료 트리거.
export class CheUisulCityHealTrigger<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends BaseGeneralTrigger<TriggerState> {
public readonly priority = TriggerPriority.Begin + 10;
public constructor(general: General<TriggerState>) {
super(general);
}
action(
context: GeneralTriggerContext<TriggerState>,
env: Record<string, unknown>
): Record<string, unknown> {
const general = context.general;
const rng = context.rng;
const logger = context.log;
if (general.injury > 0) {
general.injury = 0;
context.skill.activate('pre.부상경감', 'pre.치료');
logger?.push('<C>의술</>을 펼쳐 스스로 치료합니다!');
}
const candidates = resolveCityGenerals(general, context).filter(
(candidate) => {
if (candidate.injury <= MIN_HEAL_INJURY) {
return false;
}
if (general.nationId === 0) {
return candidate.nationId === 0;
}
return true;
}
);
const healed = candidates.filter(() => rng.nextBool(HEAL_PROBABILITY));
for (const patient of healed) {
patient.injury = 0;
}
if (healed.length === 0) {
return env;
}
const firstName = healed[0]?.name ?? '장수';
if (healed.length === 1) {
const josa = JosaUtil.pick(firstName, '을');
logger?.push(
`<C>의술</>을 펼쳐 도시의 장수 <Y>${firstName}</>${josa} 치료합니다!`
);
} else {
const otherCount = healed.length - 1;
logger?.push(
`<C>의술</>을 펼쳐 도시의 장수들 <Y>${firstName}</> 외 <C>${otherCount}</>명을 치료합니다!`
);
}
return env;
}
}