Implement crew type execution model
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
|
||||
export interface TurnCommandItemCatalogEntry {
|
||||
slot: 'horse' | 'weapon' | 'book' | 'item';
|
||||
@@ -12,6 +13,7 @@ export interface TurnCommandItemCatalogEntry {
|
||||
}
|
||||
|
||||
export interface TurnCommandEnv {
|
||||
unitSet?: UnitSetDefinition;
|
||||
develCost: number;
|
||||
minAvailableRecruitPop?: number;
|
||||
trainDelta: number;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { CrewTypeActionModule } from '../types.js';
|
||||
|
||||
export const actionModule: CrewTypeActionModule = {
|
||||
key: 'che_성벽선제',
|
||||
name: '성벽선제',
|
||||
info: '전투 가능한 성벽이라면 선제공격을 합니다.',
|
||||
war: {
|
||||
onCalcOpposeStat: (_context, statName, value) => {
|
||||
if (statName === 'cityBattleOrder') {
|
||||
return 10000;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import { WarTriggerCaller, type WarTriggerRegistry } from '@sammo-ts/logic/war/triggers.js';
|
||||
import type { CrewTypeDefinition, CrewTypeRequirement, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
|
||||
import { createCrewTypeActionRegistry } from './registry.js';
|
||||
import type { CompiledCrewType, CrewTypeActionModule, CrewTypeActionRegistry, CrewTypeCatalog } from './types.js';
|
||||
|
||||
const crewTypeWarActionRouters = new WeakSet<object>();
|
||||
|
||||
const SUPPORTED_REQUIREMENTS = new Set([
|
||||
'ReqTech',
|
||||
'ReqRegions',
|
||||
'ReqCities',
|
||||
'ReqCitiesWithCityLevel',
|
||||
'ReqHighLevelCities',
|
||||
'ReqNationAux',
|
||||
'ReqMinRelYear',
|
||||
'ReqChief',
|
||||
'ReqNotChief',
|
||||
'Impossible',
|
||||
]);
|
||||
|
||||
const validateRequirement = (
|
||||
unitSet: UnitSetDefinition,
|
||||
crewType: CrewTypeDefinition,
|
||||
requirement: CrewTypeRequirement
|
||||
): void => {
|
||||
if (!SUPPORTED_REQUIREMENTS.has(requirement.type)) {
|
||||
throw new Error(`Unknown crew type requirement in ${unitSet.id}/${crewType.id}: ${requirement.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const validateCoefficientKeys = (
|
||||
unitSet: UnitSetDefinition,
|
||||
crewType: CrewTypeDefinition,
|
||||
field: 'attackCoef' | 'defenceCoef',
|
||||
crewTypeIds: ReadonlySet<number>,
|
||||
armTypes: ReadonlySet<number>
|
||||
): void => {
|
||||
for (const rawKey of Object.keys(crewType[field])) {
|
||||
const key = Number(rawKey);
|
||||
if (!Number.isInteger(key) || (!crewTypeIds.has(key) && !armTypes.has(key))) {
|
||||
throw new Error(`Invalid ${field} key in ${unitSet.id}/${crewType.id}: ${rawKey}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const compileDefinitions = (
|
||||
unitSet: UnitSetDefinition,
|
||||
actionRegistry: CrewTypeActionRegistry,
|
||||
triggerRegistry: WarTriggerRegistry
|
||||
): Map<number, CompiledCrewType> => {
|
||||
const definitions = unitSet.crewTypes ?? [];
|
||||
if (definitions.length === 0) {
|
||||
throw new Error(`Unit set has no crew types: ${unitSet.id}`);
|
||||
}
|
||||
|
||||
const crewTypeIds = new Set<number>();
|
||||
const crewTypeNames = new Set<string>();
|
||||
const armTypes = new Set(definitions.map((crewType) => crewType.armType));
|
||||
|
||||
for (const crewType of definitions) {
|
||||
if (crewTypeIds.has(crewType.id)) {
|
||||
throw new Error(`Duplicate crew type id in ${unitSet.id}: ${crewType.id}`);
|
||||
}
|
||||
if (crewTypeNames.has(crewType.name)) {
|
||||
throw new Error(`Duplicate crew type name in ${unitSet.id}: ${crewType.name}`);
|
||||
}
|
||||
crewTypeIds.add(crewType.id);
|
||||
crewTypeNames.add(crewType.name);
|
||||
}
|
||||
|
||||
const compiled = new Map<number, CompiledCrewType>();
|
||||
for (const crewType of definitions) {
|
||||
for (const requirement of crewType.requirements) {
|
||||
validateRequirement(unitSet, crewType, requirement);
|
||||
}
|
||||
validateCoefficientKeys(unitSet, crewType, 'attackCoef', crewTypeIds, armTypes);
|
||||
validateCoefficientKeys(unitSet, crewType, 'defenceCoef', crewTypeIds, armTypes);
|
||||
|
||||
const actions: CrewTypeActionModule[] = [];
|
||||
for (const key of crewType.iActionList ?? []) {
|
||||
const action = actionRegistry.get(key);
|
||||
if (!action) {
|
||||
throw new Error(`Unknown crew type action in ${unitSet.id}/${crewType.id}: ${key}`);
|
||||
}
|
||||
actions.push(action);
|
||||
}
|
||||
|
||||
for (const key of [...(crewType.initSkillTrigger ?? []), ...(crewType.phaseSkillTrigger ?? [])]) {
|
||||
if (!triggerRegistry[key]) {
|
||||
throw new Error(`Unknown crew type war trigger in ${unitSet.id}/${crewType.id}: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
compiled.set(crewType.id, { definition: crewType, actions });
|
||||
}
|
||||
return compiled;
|
||||
};
|
||||
|
||||
const createGeneralActionRouter = <TriggerState extends GeneralTriggerState>(
|
||||
byId: ReadonlyMap<number, CompiledCrewType>
|
||||
): GeneralActionModule<TriggerState> => {
|
||||
const modules = (context: GeneralActionContext<TriggerState>) =>
|
||||
(byId.get(context.general.crewTypeId)?.actions ?? [])
|
||||
.map((action) => action.general as GeneralActionModule<TriggerState> | undefined)
|
||||
.filter((action): action is GeneralActionModule<TriggerState> => action !== undefined);
|
||||
|
||||
return {
|
||||
getPreTurnExecuteTriggerList: (context) => {
|
||||
const caller = new GeneralTriggerCaller<TriggerState>();
|
||||
for (const module of modules(context)) {
|
||||
caller.merge(module.getPreTurnExecuteTriggerList?.(context));
|
||||
}
|
||||
return caller;
|
||||
},
|
||||
onCalcDomestic: (context, turnType, varType, value, aux) => {
|
||||
let current = value;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcDomestic?.(context, turnType, varType, current, aux) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
onCalcStat: (context, statName, value, aux) => {
|
||||
let current = value;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcStat?.(context, statName, current, aux) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
onCalcOpposeStat: (context, statName, value, aux) => {
|
||||
let current = value;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcOpposeStat?.(context, statName, current, aux) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
onCalcStrategic: (context, turnType, varType, value) => {
|
||||
let current = value;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcStrategic?.(context, turnType, varType, current) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
onCalcNationalIncome: (context, type, amount) => {
|
||||
let current = amount;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcNationalIncome?.(context, type, current) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
onArbitraryAction: (context, actionType, phase, aux) => {
|
||||
let current = aux ?? null;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onArbitraryAction?.(context, actionType, phase, current) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
} satisfies GeneralActionModule<TriggerState>;
|
||||
};
|
||||
|
||||
const createWarActionRouter = <TriggerState extends GeneralTriggerState>(
|
||||
byId: ReadonlyMap<number, CompiledCrewType>,
|
||||
triggerRegistry: WarTriggerRegistry
|
||||
): WarActionModule<TriggerState> => {
|
||||
const compiled = (context: WarActionContext<TriggerState>) => byId.get(context.general.crewTypeId);
|
||||
const modules = (context: WarActionContext<TriggerState>) =>
|
||||
(compiled(context)?.actions ?? [])
|
||||
.map((action) => action.war as WarActionModule<TriggerState> | undefined)
|
||||
.filter((action): action is WarActionModule<TriggerState> => action !== undefined);
|
||||
const appendDefinitionTriggers = (
|
||||
caller: WarTriggerCaller,
|
||||
context: WarActionContext<TriggerState>,
|
||||
keys: readonly string[]
|
||||
): void => {
|
||||
if (!context.unit) {
|
||||
if (keys.length > 0) {
|
||||
throw new Error('Crew type war triggers require a battle unit context');
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const key of keys) {
|
||||
const trigger = triggerRegistry[key]?.(context.unit);
|
||||
if (!trigger) {
|
||||
throw new Error(`Unknown crew type war trigger: ${key}`);
|
||||
}
|
||||
if (trigger instanceof WarTriggerCaller) {
|
||||
caller.merge(trigger);
|
||||
} else {
|
||||
caller.append(trigger);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const router = {
|
||||
getBattleInitTriggerList: (context) => {
|
||||
const caller = new WarTriggerCaller();
|
||||
appendDefinitionTriggers(caller, context, compiled(context)?.definition.initSkillTrigger ?? []);
|
||||
for (const module of modules(context)) {
|
||||
caller.merge(module.getBattleInitTriggerList?.(context));
|
||||
}
|
||||
return caller;
|
||||
},
|
||||
getBattlePhaseTriggerList: (context) => {
|
||||
const caller = new WarTriggerCaller();
|
||||
appendDefinitionTriggers(caller, context, compiled(context)?.definition.phaseSkillTrigger ?? []);
|
||||
for (const module of modules(context)) {
|
||||
caller.merge(module.getBattlePhaseTriggerList?.(context));
|
||||
}
|
||||
return caller;
|
||||
},
|
||||
onCalcStat: (context, statName, value, aux) => {
|
||||
let current = value;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcStat?.(context, statName, current, aux) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
onCalcOpposeStat: (context, statName, value, aux) => {
|
||||
let current = value;
|
||||
for (const module of modules(context)) {
|
||||
current = module.onCalcOpposeStat?.(context, statName, current, aux) ?? current;
|
||||
}
|
||||
return current;
|
||||
},
|
||||
getWarPowerMultiplier: (context, unit, oppose) => {
|
||||
let attack = 1;
|
||||
let defence = 1;
|
||||
for (const module of modules(context)) {
|
||||
const [attackMultiplier, defenceMultiplier] = module.getWarPowerMultiplier?.(context, unit, oppose) ?? [
|
||||
1, 1,
|
||||
];
|
||||
attack *= attackMultiplier;
|
||||
defence *= defenceMultiplier;
|
||||
}
|
||||
return [attack, defence];
|
||||
},
|
||||
} satisfies WarActionModule<TriggerState>;
|
||||
crewTypeWarActionRouters.add(router);
|
||||
return router;
|
||||
};
|
||||
|
||||
export const isCrewTypeWarActionRouter = <TriggerState extends GeneralTriggerState>(
|
||||
module: WarActionModule<TriggerState>
|
||||
): boolean => crewTypeWarActionRouters.has(module);
|
||||
|
||||
export const compileCrewTypeCatalog = (
|
||||
unitSet: UnitSetDefinition,
|
||||
triggerRegistry: WarTriggerRegistry,
|
||||
actionRegistry: CrewTypeActionRegistry = createCrewTypeActionRegistry()
|
||||
): CrewTypeCatalog => {
|
||||
const byId = compileDefinitions(unitSet, actionRegistry, triggerRegistry);
|
||||
return {
|
||||
unitSet,
|
||||
byId,
|
||||
generalActionModule: createGeneralActionRouter(byId),
|
||||
warActionModule: createWarActionRouter(byId, triggerRegistry),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './types.js';
|
||||
export * from './registry.js';
|
||||
export * from './catalog.js';
|
||||
@@ -0,0 +1,8 @@
|
||||
import { actionModule as castleFirst } from './actions/che_성벽선제.js';
|
||||
import type { CrewTypeActionModule, CrewTypeActionRegistry } from './types.js';
|
||||
|
||||
export const CREW_TYPE_ACTION_KEYS = ['che_성벽선제'] as const;
|
||||
|
||||
export const createCrewTypeActionRegistry = (
|
||||
modules: readonly CrewTypeActionModule[] = [castleFirst]
|
||||
): CrewTypeActionRegistry => new Map(modules.map((module) => [module.key, module]));
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { CrewTypeDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
|
||||
export interface CrewTypeActionModule {
|
||||
key: string;
|
||||
name: string;
|
||||
info: string;
|
||||
general?: GeneralActionModule;
|
||||
war?: WarActionModule;
|
||||
}
|
||||
|
||||
export type CrewTypeActionRegistry = ReadonlyMap<string, CrewTypeActionModule>;
|
||||
|
||||
export interface CompiledCrewType {
|
||||
definition: CrewTypeDefinition;
|
||||
actions: readonly CrewTypeActionModule[];
|
||||
}
|
||||
|
||||
export interface CrewTypeCatalog {
|
||||
unitSet: UnitSetDefinition;
|
||||
byId: ReadonlyMap<number, CompiledCrewType>;
|
||||
generalActionModule: GeneralActionModule;
|
||||
warActionModule: WarActionModule;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export * from './domain/entities.js';
|
||||
export type { RandomGenerator } from '@sammo-ts/common';
|
||||
export * from './actions/index.js';
|
||||
export * from './constraints/index.js';
|
||||
export * from './crewType/index.js';
|
||||
export * from './diplomacy/index.js';
|
||||
export * from './economy/index.js';
|
||||
export * from './logging/index.js';
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { CrewTypeDefinition, CrewTypeRequirement, MapDefinition, UnitSetDefinition } from './types.js';
|
||||
import {
|
||||
asNullableStringArray,
|
||||
asNumber,
|
||||
asRecord,
|
||||
asString,
|
||||
asStringArray,
|
||||
isRecord,
|
||||
} from '@sammo-ts/common';
|
||||
import { asNullableStringArray, asNumber, asRecord, asString, asStringArray, isRecord } from '@sammo-ts/common';
|
||||
import { UnitSetDefinitionInputSchema } from '../resources/unitSetSchema.js';
|
||||
|
||||
const DEFAULT_REGION_MAP: Record<string, number> = {
|
||||
@@ -181,6 +174,14 @@ export const getTechAbility = (tech: number): number => getTechLevel(tech) * 25;
|
||||
|
||||
export const getTechCost = (tech: number): number => 1 + getTechLevel(tech) * 0.15;
|
||||
|
||||
export const getCrewTypePickScore = (crewType: CrewTypeDefinition, tech: number, armPerPhase: number): number => {
|
||||
let score = armPerPhase + crewType.attack + crewType.defence + getTechAbility(tech) * 2;
|
||||
score *= 1 + crewType.speed / 2;
|
||||
score /= Math.max(1 - crewType.avoid / 100, 0.1);
|
||||
score *= 1 + crewType.magicCoef / 2;
|
||||
return score;
|
||||
};
|
||||
|
||||
export interface CrewTypeAvailabilityContext {
|
||||
general: General;
|
||||
nation: Nation | null;
|
||||
|
||||
Reference in New Issue
Block a user