feat: port scenario action effects

This commit is contained in:
2026-07-30 19:00:04 +00:00
parent 7f31459385
commit c68d992046
39 changed files with 1131 additions and 95 deletions
+7 -5
View File
@@ -27,6 +27,8 @@ import {
WAR_TRAIT_KEYS,
} from './traits/index.js';
import type { NationTraitModule } from './traits/nation/index.js';
import { createScenarioEffectActionModules } from './scenarioEffect.js';
import type { ScenarioEffectKey } from '@sammo-ts/logic/scenario/scenarioEffect.js';
export interface ActionModuleBundle<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: RefOrderedActionStack<GeneralActionModule<TriggerState>>;
@@ -82,7 +84,8 @@ export const createRefOrderedActionStack = <Module>(slots: RefActionSlots<Module
// General::getActionList와 같은 소유권 순서로 실제 턴과 시뮬레이터의 모듈을 조립한다.
export const loadActionModuleBundle = async <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
unitSet?: UnitSetDefinition
unitSet?: UnitSetDefinition,
scenarioEffect?: ScenarioEffectKey | null
): Promise<ActionModuleBundle<TriggerState>> => {
const [domestic, war, personality, nation, itemModules] = await Promise.all([
loadDomesticTraitModules([...DOMESTIC_TRAIT_KEYS]),
@@ -95,6 +98,7 @@ export const loadActionModuleBundle = async <TriggerState extends GeneralTrigger
const officer = createOfficerLevelActionModules<TriggerState>();
const items = createItemActionModules(createItemModuleRegistry(itemModules));
const inherit = createInheritBuffModules();
const scenario = createScenarioEffectActionModules<TriggerState>(scenarioEffect);
const crewTypeCatalog = unitSet?.crewTypes?.length
? compileCrewTypeCatalog(unitSet, createCrewTypeWarTriggerRegistry())
: null;
@@ -110,8 +114,7 @@ export const loadActionModuleBundle = async <TriggerState extends GeneralTrigger
? (crewTypeCatalog.generalActionModule as GeneralActionModule<TriggerState>)
: null,
inheritance: inherit.general as GeneralActionModule<TriggerState>,
// scenarioEffect는 현재 core runtime module이 없어 명시적으로 빈 slot입니다.
scenario: null,
scenario: scenario.general,
items: items.general,
}),
war: createRefOrderedActionStack<WarActionModule<TriggerState>>({
@@ -122,8 +125,7 @@ export const loadActionModuleBundle = async <TriggerState extends GeneralTrigger
personality: new TraitWarActionRouter('personality', traitCatalog),
crewType: crewTypeCatalog ? (crewTypeCatalog.warActionModule as WarActionModule<TriggerState>) : null,
inheritance: inherit.war as WarActionModule<TriggerState>,
// ref의 scenarioEffect 위치를 보존하되 미이식 module은 별도 gap으로 남깁니다.
scenario: null,
scenario: scenario.war,
items: items.war,
}),
itemModules,
@@ -3,4 +3,5 @@ export * from './general.js';
export * from './types.js';
export * from './officerLevel.js';
export * from './bundle.js';
export * from './scenarioEffect.js';
export * from './traits/index.js';
@@ -0,0 +1,84 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_전멸시페이즈증가 } from '@sammo-ts/logic/war/triggers/che_전멸시페이즈증가.js';
import { WarUnitCity } from '@sammo-ts/logic/war/units.js';
import { normalizeScenarioEffect } from '@sammo-ts/logic/scenario/scenarioEffect.js';
export type { ScenarioEffectKey } from '@sammo-ts/logic/scenario/scenarioEffect.js';
export interface ScenarioEffectActionModules<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: GeneralActionModule<TriggerState> | null;
war: WarActionModule<TriggerState> | null;
}
const MORE_EFFECT_DOMESTIC_ACTIONS = new Set(['상업', '농업', '치안', '기술', '성벽', '수비', '인구', '민심']);
const createDefenceThresholdGeneralModule = <
TriggerState extends GeneralTriggerState,
>(): GeneralActionModule<TriggerState> => ({
onCalcDomestic: (_context, turnType, _varType, value) => (turnType === 'changeDefenceTrain' ? 0 : value),
});
const createAdvanceTriggerWarModule = <TriggerState extends GeneralTriggerState>(
includeCityWarPower: boolean
): WarActionModule<TriggerState> => ({
getWarPowerMultiplier: (_context, unit, oppose) => {
if (!includeCityWarPower && (unit instanceof WarUnitCity || oppose instanceof WarUnitCity)) {
return [1, 1];
}
return unit.isAttacker() ? [1.4, 0.7143] : [1, 1];
},
getBattlePhaseTriggerList: (context) => {
const unit = context.unit;
return unit ? new WarTriggerCaller(new che_전멸시페이즈증가(unit)) : null;
},
});
const createMoreEffectGeneralModule = <
TriggerState extends GeneralTriggerState,
>(): GeneralActionModule<TriggerState> => ({
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === 'changeDefenceTrain') {
return 0;
}
return varType === 'score' && MORE_EFFECT_DOMESTIC_ACTIONS.has(turnType) ? value * 2 : value;
},
// ref에도 정의되어 있지만 실제 월간 수입 경로는 General이 아니라
// nation type module만 호출합니다. protocol 보존용이며 월간 경로에는
// 이 general hook을 연결하지 않습니다.
onCalcNationalIncome: (_context, type, amount) => {
if (type === 'gold' || type === 'rice' || (type === 'pop' && amount > 0)) {
return amount * 2;
}
return amount;
},
});
export const createScenarioEffectActionModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
scenarioEffect?: string | null
): ScenarioEffectActionModules<TriggerState> => {
const normalizedEffect = normalizeScenarioEffect(scenarioEffect);
if (!normalizedEffect) {
return { general: null, war: null };
}
switch (normalizedEffect) {
case 'event_UnlimitedDefenceThresholdChange':
return {
general: createDefenceThresholdGeneralModule<TriggerState>(),
war: null,
};
case 'event_StrongAttacker':
return {
general: createDefenceThresholdGeneralModule<TriggerState>(),
war: createAdvanceTriggerWarModule<TriggerState>(false),
};
case 'event_MoreEffect':
return {
general: createMoreEffectGeneralModule<TriggerState>(),
war: createAdvanceTriggerWarModule<TriggerState>(true),
};
}
};
+2 -1
View File
@@ -13,7 +13,8 @@ export type TriggerDomesticActionType =
| '기술'
| '모병'
| '단련'
| '조달';
| '조달'
| 'changeDefenceTrain';
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';