Implement crew type execution model

This commit is contained in:
2026-07-25 05:16:11 +00:00
parent 2824e5af13
commit c5da507df9
24 changed files with 1165 additions and 48 deletions
+8
View File
@@ -36,6 +36,14 @@ export class WarCrewType {
return this.definition.rice;
}
get magicCoef(): number {
return this.definition.magicCoef;
}
get cost(): number {
return this.definition.cost;
}
public reqCities(): boolean {
return this.definition.requirements.some((req) => req.type === 'ReqCities');
}
@@ -0,0 +1,26 @@
import type { WarTriggerRegistry } from './triggers.js';
import { che_기병병종전투 } from './triggers/che_기병병종전투.js';
import { che_방어력증가5p } from './triggers/che_방어력증가5p.js';
import { che_선제사격발동, che_선제사격시도 } from './triggers/che_선제사격.js';
import { che_성벽부상무효 } from './triggers/che_성벽부상무효.js';
import { che_저지, che_저지_시도 } from './triggers/che_저지.js';
export const CREW_TYPE_WAR_TRIGGER_KEYS = [
'che_성벽부상무효',
'che_기병병종전투',
'che_방어력증가5p',
'che_선제사격시도',
'che_선제사격발동',
'che_저지시도',
'che_저지발동',
] as const;
export const createCrewTypeWarTriggerRegistry = (): WarTriggerRegistry => ({
che_성벽부상무효: (unit) => new che_성벽부상무효(unit),
che_기병병종전투: (unit) => new che_기병병종전투(unit),
che_방어력증가5p: (unit) => new che_방어력증가5p(unit),
che_선제사격시도: (unit) => new che_선제사격시도(unit),
che_선제사격발동: (unit) => new che_선제사격발동(unit),
che_저지시도: (unit) => new che_저지_시도(unit),
che_저지발동: (unit) => new che_저지(unit),
});
+37 -18
View File
@@ -1,10 +1,12 @@
import { JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import type { City, General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import { compileCrewTypeCatalog, isCrewTypeWarActionRouter } from '@sammo-ts/logic/crewType/catalog.js';
import { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
import { buildCrewTypeIndex as buildCrewTypeDefinitionIndex } from '@sammo-ts/logic/world/unitSet.js';
import { WarActionPipeline } from './actions.js';
import { WarActionPipeline, type WarActionModule } from './actions.js';
import { createCrewTypeWarTriggerRegistry } from './crewTypeTriggers.js';
import { WarCrewType } from './crewType.js';
import { WarTriggerCaller, createWarTriggerEnv, type WarTriggerRegistry } from './triggers.js';
import type { WarBattleInput, WarBattleOutcome, WarGeneralInput, WarUnitReport } from './types.js';
@@ -37,22 +39,26 @@ const buildWarCrewTypeIndex = (unitSet: WarBattleInput['unitSet']): Map<number,
};
const createPipeline = <TriggerState extends GeneralTriggerState>(
input: WarGeneralInput<TriggerState>
): WarActionPipeline<TriggerState> => new WarActionPipeline(input.modules ?? []);
input: WarGeneralInput<TriggerState>,
crewTypeModule: WarActionModule<TriggerState>
): WarActionPipeline<TriggerState> => {
const modules = input.modules ?? [];
if (modules.some((module) => module && isCrewTypeWarActionRouter(module))) {
return new WarActionPipeline(modules);
}
return new WarActionPipeline([crewTypeModule, ...modules]);
};
const appendCrewTypeTriggers = (
caller: WarTriggerCaller,
unit: WarUnit,
names: string[],
registry: WarTriggerRegistry | undefined
registry: WarTriggerRegistry
): void => {
if (!registry) {
return;
}
for (const name of names) {
const factory = registry[name];
if (!factory) {
continue;
throw new Error(`Unknown crew type war trigger: ${name}`);
}
const trigger = factory(unit);
if (!trigger) {
@@ -66,24 +72,28 @@ const appendCrewTypeTriggers = (
}
};
const buildBattleInitTriggers = (unit: WarUnit, registry: WarTriggerRegistry | undefined): WarTriggerCaller => {
const buildBattleInitTriggers = (unit: WarUnit, registry: WarTriggerRegistry): WarTriggerCaller => {
const caller = new WarTriggerCaller();
if (unit instanceof WarUnitGeneral) {
const context = unit.getActionContext();
caller.merge(unit.getActionPipeline().getBattleInitTriggerList(context));
} else {
appendCrewTypeTriggers(caller, unit, unit.getCrewType().initSkillTrigger, registry);
}
appendCrewTypeTriggers(caller, unit, unit.getCrewType().initSkillTrigger, registry);
return caller;
};
const buildBattlePhaseTriggers = (unit: WarUnit, registry: WarTriggerRegistry | undefined): WarTriggerCaller => {
const buildBattlePhaseTriggers = (unit: WarUnit, registry: WarTriggerRegistry): WarTriggerCaller => {
const caller = new WarTriggerCaller();
if (unit instanceof WarUnitGeneral) {
appendCrewTypeTriggers(caller, unit, ['che_필살'], registry);
if (registry['che_필살']) {
appendCrewTypeTriggers(caller, unit, ['che_필살'], registry);
}
const context = unit.getActionContext();
caller.merge(unit.getActionPipeline().getBattlePhaseTriggerList(context));
} else {
appendCrewTypeTriggers(caller, unit, unit.getCrewType().phaseSkillTrigger, registry);
}
appendCrewTypeTriggers(caller, unit, unit.getCrewType().phaseSkillTrigger, registry);
return caller;
};
@@ -196,10 +206,14 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
// process_war.php 전투 루프를 순수 로직으로 이식한다.
const rng = input.rng ?? new RandUtil(LiteHashDRBG.build(input.seed ?? ''));
const loggerFactory = input.loggerFactory ?? defaultLoggerFactory;
const triggerRegistry = input.triggerRegistry;
const triggerRegistry: WarTriggerRegistry = {
...createCrewTypeWarTriggerRegistry(),
...(input.triggerRegistry ?? {}),
};
const crewTypeCatalog = compileCrewTypeCatalog(input.unitSet, triggerRegistry);
const crewTypeIndex = buildWarCrewTypeIndex(input.unitSet);
const attackerPipeline = createPipeline(input.attacker);
const attackerPipeline = createPipeline(input.attacker, crewTypeCatalog.warActionModule);
const attackerLogger =
input.attacker.logger ??
loggerFactory({
@@ -251,7 +265,7 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
false,
resolveCrewType(crewTypeIndex, defender.general.crewTypeId),
defenderLogger,
createPipeline(defender)
createPipeline(defender, crewTypeCatalog.warActionModule)
);
if (computeBattleOrder(unit, attackerUnit) <= 0) {
continue;
@@ -601,9 +615,14 @@ export const resolveDefenderOrder = <TriggerState extends GeneralTriggerState =
): number[] => {
const rng = input.rng ?? new RandUtil(LiteHashDRBG.build(input.seed ?? ''));
const loggerFactory = input.loggerFactory ?? defaultLoggerFactory;
const triggerRegistry: WarTriggerRegistry = {
...createCrewTypeWarTriggerRegistry(),
...(input.triggerRegistry ?? {}),
};
const crewTypeCatalog = compileCrewTypeCatalog(input.unitSet, triggerRegistry);
const crewTypeIndex = buildWarCrewTypeIndex(input.unitSet);
const attackerPipeline = createPipeline(input.attacker);
const attackerPipeline = createPipeline(input.attacker, crewTypeCatalog.warActionModule);
const attackerLogger =
input.attacker.logger ??
loggerFactory({
@@ -640,7 +659,7 @@ export const resolveDefenderOrder = <TriggerState extends GeneralTriggerState =
false,
resolveCrewType(crewTypeIndex, defender.general.crewTypeId),
defenderLogger,
createPipeline(defender)
createPipeline(defender, crewTypeCatalog.warActionModule)
);
if (computeBattleOrder(unit, attackerUnit) <= 0) {
continue;
+1
View File
@@ -6,4 +6,5 @@ export * from './units.js';
export * from './triggers.js';
export * from './triggers/index.js';
export * from './crewType.js';
export * from './crewTypeTriggers.js';
export * from './utils.js';
@@ -0,0 +1,22 @@
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
import { WarUnitCity, type WarUnit } from '@sammo-ts/logic/war/units.js';
export class che_기병병종전투 extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Final + 100);
}
protected actionWar(self: WarUnit, oppose: WarUnit): boolean {
if (!self.isAttacker()) {
oppose.multiplyWarPowerMultiply(1.02);
self.multiplyWarPowerMultiply(0.97);
} else if (oppose instanceof WarUnitCity) {
self.multiplyWarPowerMultiply(0.9);
} else {
oppose.multiplyWarPowerMultiply(0.97);
self.multiplyWarPowerMultiply(1.02);
}
return true;
}
}
@@ -0,0 +1,16 @@
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
export class che_방어력증가5p extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Final + 200);
}
protected actionWar(self: WarUnit, oppose: WarUnit): boolean {
if (!self.isAttacker()) {
oppose.multiplyWarPowerMultiply(1 / 1.05);
}
return true;
}
}
@@ -0,0 +1,55 @@
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
export class che_선제사격시도 extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Begin + 50);
}
protected actionWar(self: WarUnit, oppose: WarUnit): boolean {
if (self.getPhase() !== 0 && oppose.getPhase() !== 0) {
return true;
}
if (self.hasActivatedSkill('선제') || self.hasActivatedSkillOnLog('선제')) {
return true;
}
self.activateSkill('특수', '선제');
return true;
}
}
export class che_선제사격발동 extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Begin + 51);
}
protected actionWar(self: WarUnit, oppose: WarUnit): boolean {
if (!self.hasActivatedSkill('선제')) {
return true;
}
if (oppose.hasActivatedSkill('선제') && oppose.isAttacker()) {
return true;
}
self.addPhase(-1);
oppose.addPhase(-1);
if (oppose.hasActivatedSkill('선제')) {
self.multiplyWarPowerMultiply(2 / 3);
oppose.multiplyWarPowerMultiply(2 / 3);
oppose.getLogger().pushGeneralBattleDetailLog('서로 <C>선제 사격</>을 주고 받았다!</>', LogFormat.PLAIN);
self.getLogger().pushGeneralBattleDetailLog('서로 <C>선제 사격</>을 주고 받았다!</>', LogFormat.PLAIN);
return true;
}
oppose.multiplyWarPowerMultiply(0);
self.multiplyWarPowerMultiply(2 / 3);
self.activateSkill('회피불가', '필살불가', '계략불가');
oppose.activateSkill('회피불가', '필살불가', '격노불가', '계략불가');
oppose.getLogger().pushGeneralBattleDetailLog('상대에게 <R>선제 사격</>을 받았다!</>', LogFormat.PLAIN);
self.getLogger().pushGeneralBattleDetailLog('상대에게 <C>선제 사격</>을 했다!</>', LogFormat.PLAIN);
return true;
}
}
@@ -0,0 +1,17 @@
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
import { WarUnitCity, WarUnitGeneral, type WarUnit } from '@sammo-ts/logic/war/units.js';
export class che_성벽부상무효 extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Begin + 150);
}
protected actionWar(self: WarUnit, oppose: WarUnit): boolean {
if (!(self instanceof WarUnitGeneral) || !(oppose instanceof WarUnitCity)) {
return true;
}
self.activateSkill('부상무효');
return true;
}
}
+13 -3
View File
@@ -7,8 +7,18 @@ export class che_저지_시도 extends BaseWarUnitTrigger {
constructor(unit: WarUnit, raiseType: number = 0) {
super(unit, TriggerPriority.Pre, raiseType);
}
protected actionWar(u: WarUnit): boolean {
u.activateSkill('특수', '저지');
protected actionWar(self: WarUnit): boolean {
if (!(self instanceof WarUnitGeneral) || self.isAttacker()) {
return true;
}
if (self.hasActivatedSkill('특수') || self.hasActivatedSkill('저지불가')) {
return true;
}
const ratio = self.getComputedAtmos() + self.getComputedTrain();
if (self.rng.nextBool(ratio / 400)) {
self.activateSkill('특수', '저지');
}
return true;
}
}
@@ -40,7 +50,7 @@ export class che_저지 extends BaseWarUnitTrigger {
}
self.getLogger().pushGeneralBattleDetailLog('상대를 <C>저지</>했다!', LogFormat.PLAIN);
oppose.getLogger().pushGeneralBattleDetailLog('<R>저지</>당했다!', LogFormat.PLAIN);
oppose.getLogger().pushGeneralBattleDetailLog('저지</>당했다!', LogFormat.PLAIN);
const calcDamage = oppose.getWarPower() * 0.9;
if (self instanceof WarUnitGeneral) {