feat: port scenario action effects
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,8 @@ export type TriggerDomesticActionType =
|
||||
| '기술'
|
||||
| '모병'
|
||||
| '단련'
|
||||
| '조달';
|
||||
| '조달'
|
||||
| 'changeDefenceTrain';
|
||||
|
||||
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { NationTraitModule } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
|
||||
import type { RefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js';
|
||||
import type { ScenarioEffectKey } from '@sammo-ts/logic/scenario/scenarioEffect.js';
|
||||
|
||||
export interface TurnCommandItemCatalogEntry {
|
||||
slot: 'horse' | 'weapon' | 'book' | 'item';
|
||||
@@ -17,6 +18,7 @@ export interface TurnCommandItemCatalogEntry {
|
||||
|
||||
export interface TurnCommandEnv {
|
||||
unitSet?: UnitSetDefinition;
|
||||
scenarioEffect?: ScenarioEffectKey | null;
|
||||
develCost: number;
|
||||
minAvailableRecruitPop?: number;
|
||||
trainDelta: number;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { SCENARIO_EFFECT_KEYS } from '../scenario/scenarioEffect.js';
|
||||
|
||||
export const ScenarioStatBlockSchema = z
|
||||
.object({
|
||||
@@ -19,6 +20,12 @@ export const ScenarioDefaultsInputSchema = z.object({
|
||||
|
||||
export const ScenarioExtendsInputSchema = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
|
||||
|
||||
const ScenarioConstInputSchema = z
|
||||
.object({
|
||||
scenarioEffect: z.union([z.enum(SCENARIO_EFFECT_KEYS), z.literal(''), z.literal('None'), z.null()]).optional(),
|
||||
})
|
||||
.catchall(z.unknown());
|
||||
|
||||
const ScenarioBodyInputSchema = z
|
||||
.object({
|
||||
extends: ScenarioExtendsInputSchema.optional(),
|
||||
@@ -29,7 +36,7 @@ const ScenarioBodyInputSchema = z
|
||||
iconPath: z.string().optional(),
|
||||
stat: ScenarioStatBlockSchema.optional(),
|
||||
map: z.record(z.string(), z.unknown()).optional(),
|
||||
const: z.record(z.string(), z.unknown()).optional(),
|
||||
const: ScenarioConstInputSchema.optional(),
|
||||
nation: z.array(z.unknown()).optional(),
|
||||
diplomacy: z.array(z.unknown()).optional(),
|
||||
general: z.array(z.unknown()).optional(),
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './types.js';
|
||||
export * from './parseScenario.js';
|
||||
export * from './scenarioEffect.js';
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ScenarioNation,
|
||||
ScenarioStatBlock,
|
||||
} from './types.js';
|
||||
import { normalizeScenarioEffect } from './scenarioEffect.js';
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
@@ -45,12 +46,9 @@ const parseScenarioEnvironment = (mapConfig: UnknownRecord, constConfig: Unknown
|
||||
const merged = { ...mapConfig, ...constConfig };
|
||||
const mapName = asString(merged.mapName, 'che');
|
||||
const unitSet = asString(merged.unitSet, 'che');
|
||||
const scenarioEffect =
|
||||
typeof merged.scenarioEffect === 'string' || merged.scenarioEffect === null ? merged.scenarioEffect : undefined;
|
||||
|
||||
const result: ScenarioEnvironment = { mapName, unitSet };
|
||||
if (scenarioEffect !== undefined) {
|
||||
result.scenarioEffect = scenarioEffect;
|
||||
if (Object.hasOwn(merged, 'scenarioEffect')) {
|
||||
result.scenarioEffect = normalizeScenarioEffect(merged.scenarioEffect);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export const SCENARIO_EFFECT_KEYS = [
|
||||
'event_UnlimitedDefenceThresholdChange',
|
||||
'event_StrongAttacker',
|
||||
'event_MoreEffect',
|
||||
] as const;
|
||||
|
||||
export type ScenarioEffectKey = (typeof SCENARIO_EFFECT_KEYS)[number];
|
||||
|
||||
const DEFENCE_TRAIN_PENALTY_WAIVER_EFFECTS = new Set<ScenarioEffectKey>([
|
||||
'event_UnlimitedDefenceThresholdChange',
|
||||
'event_StrongAttacker',
|
||||
'event_MoreEffect',
|
||||
]);
|
||||
|
||||
export const isScenarioEffectKey = (value: string): value is ScenarioEffectKey =>
|
||||
SCENARIO_EFFECT_KEYS.includes(value as ScenarioEffectKey);
|
||||
|
||||
export const normalizeScenarioEffect = (value: unknown): ScenarioEffectKey | null => {
|
||||
if (value === undefined || value === null || value === '' || value === 'None') {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'string' && isScenarioEffectKey(value)) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Unknown scenario effect: ${String(value)}`);
|
||||
};
|
||||
|
||||
export const isDefenceTrainPenaltyWaivedByScenarioEffect = (value: string | null | undefined): boolean => {
|
||||
const effect = normalizeScenarioEffect(value);
|
||||
return effect !== null && DEFENCE_TRAIN_PENALTY_WAIVER_EFFECTS.has(effect);
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export interface ScenarioDefaults {
|
||||
export interface ScenarioEnvironment {
|
||||
mapName: string;
|
||||
unitSet: string;
|
||||
scenarioEffect?: string | null;
|
||||
scenarioEffect?: ScenarioEffectKey | null;
|
||||
}
|
||||
|
||||
export interface ScenarioConfig {
|
||||
@@ -86,3 +86,4 @@ export interface ScenarioDefinition {
|
||||
initialEvents: unknown[];
|
||||
ignoreDefaultEvents: boolean;
|
||||
}
|
||||
import type { ScenarioEffectKey } from './scenarioEffect.js';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* ref che_전멸시페이즈증가.
|
||||
*
|
||||
* 이전 수비자를 격파한 공격자가 phase를 소비한 상태로 새 수비자
|
||||
* (phase 0)와 맞붙을 때 다음 phase 하나를 보너스로 얻습니다.
|
||||
*/
|
||||
export class che_전멸시페이즈증가 extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit) {
|
||||
super(unit, TriggerPriority.Post + 800);
|
||||
}
|
||||
|
||||
protected actionWar(
|
||||
self: WarUnit,
|
||||
oppose: WarUnit,
|
||||
_selfEnv: Record<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): boolean {
|
||||
if (self.getPhase() === 0 || oppose.getPhase() !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.addBonusPhase(1);
|
||||
self.getLogger().pushGeneralBattleDetailLog('적군의 전멸에 <C>진격</>이 이어집니다!', LogFormat.PLAIN);
|
||||
oppose.getLogger().pushGeneralBattleDetailLog('아군의 전멸에 상대의 <R>진격</>이 이어집니다!', LogFormat.PLAIN);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user