refactor(logic): replace arbitrary action hooks with typed events
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { compileCrewTypeCatalog } from '@sammo-ts/logic/crewType/catalog.js';
|
||||
import { createCrewTypeWarTriggerRegistry } from '@sammo-ts/logic/war/crewTypeTriggers.js';
|
||||
import { createInheritBuffModules } from '@sammo-ts/logic/inheritance/inheritBuff.js';
|
||||
import {
|
||||
createItemActionModules,
|
||||
createItemModuleRegistry,
|
||||
ITEM_KEYS,
|
||||
loadItemModules,
|
||||
type ItemModule,
|
||||
} from '@sammo-ts/logic/items/index.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { GeneralActionModule } from './general.js';
|
||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import { createOfficerLevelActionModules } from './officerLevel.js';
|
||||
import {
|
||||
createTraitCatalog,
|
||||
DOMESTIC_TRAIT_KEYS,
|
||||
loadDomesticTraitModules,
|
||||
loadNationTraitModules,
|
||||
loadPersonalityTraitModules,
|
||||
loadWarTraitModules,
|
||||
NATION_TRAIT_KEYS,
|
||||
PERSONALITY_TRAIT_KEYS,
|
||||
TraitGeneralActionRouter,
|
||||
TraitWarActionRouter,
|
||||
WAR_TRAIT_KEYS,
|
||||
} from './traits/index.js';
|
||||
import type { NationTraitModule } from './traits/nation/index.js';
|
||||
|
||||
export interface ActionModuleBundle<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
general: RefOrderedActionStack<GeneralActionModule<TriggerState>>;
|
||||
war: RefOrderedActionStack<WarActionModule<TriggerState>>;
|
||||
itemModules: ItemModule<TriggerState>[];
|
||||
nationTraitModules: NationTraitModule[];
|
||||
}
|
||||
|
||||
const refActionOrderBrand: unique symbol = Symbol('RefOrderedActionStack');
|
||||
|
||||
export type RefOrderedActionStack<Module> = ReadonlyArray<Module> & {
|
||||
readonly [refActionOrderBrand]: true;
|
||||
};
|
||||
|
||||
interface RefActionSlots<Module> {
|
||||
nation: Module;
|
||||
officer: Module;
|
||||
domestic: Module;
|
||||
war: Module;
|
||||
personality: Module;
|
||||
crewType: Module | null;
|
||||
inheritance: Module;
|
||||
scenario: Module | null;
|
||||
items: readonly Module[];
|
||||
}
|
||||
|
||||
const markRefOrderedActionStack: <Module>(
|
||||
stack: Module[]
|
||||
) => asserts stack is Module[] & { readonly [refActionOrderBrand]: true } = (stack) => {
|
||||
Object.defineProperty(stack, refActionOrderBrand, {
|
||||
value: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
});
|
||||
};
|
||||
|
||||
// ref General::getActionList()의 소유권 순서를 한 곳에서만 조립합니다.
|
||||
export const createRefOrderedActionStack = <Module>(slots: RefActionSlots<Module>): RefOrderedActionStack<Module> => {
|
||||
const stack = [
|
||||
slots.nation,
|
||||
slots.officer,
|
||||
slots.domestic,
|
||||
slots.war,
|
||||
slots.personality,
|
||||
...(slots.crewType ? [slots.crewType] : []),
|
||||
slots.inheritance,
|
||||
...(slots.scenario ? [slots.scenario] : []),
|
||||
...slots.items,
|
||||
];
|
||||
markRefOrderedActionStack(stack);
|
||||
return stack;
|
||||
};
|
||||
|
||||
// General::getActionList와 같은 소유권 순서로 실제 턴과 시뮬레이터의 모듈을 조립한다.
|
||||
export const loadActionModuleBundle = async <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
unitSet?: UnitSetDefinition
|
||||
): Promise<ActionModuleBundle<TriggerState>> => {
|
||||
const [domestic, war, personality, nation, itemModules] = await Promise.all([
|
||||
loadDomesticTraitModules([...DOMESTIC_TRAIT_KEYS]),
|
||||
loadWarTraitModules([...WAR_TRAIT_KEYS]),
|
||||
loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
|
||||
loadNationTraitModules([...NATION_TRAIT_KEYS]),
|
||||
loadItemModules([...ITEM_KEYS]) as Promise<ItemModule<TriggerState>[]>,
|
||||
]);
|
||||
const traitCatalog = createTraitCatalog<TriggerState>({ domestic, war, personality, nation });
|
||||
const officer = createOfficerLevelActionModules<TriggerState>();
|
||||
const items = createItemActionModules(createItemModuleRegistry(itemModules));
|
||||
const inherit = createInheritBuffModules();
|
||||
const crewTypeCatalog = unitSet?.crewTypes?.length
|
||||
? compileCrewTypeCatalog(unitSet, createCrewTypeWarTriggerRegistry())
|
||||
: null;
|
||||
|
||||
return {
|
||||
general: createRefOrderedActionStack<GeneralActionModule<TriggerState>>({
|
||||
nation: new TraitGeneralActionRouter('nation', traitCatalog),
|
||||
officer: officer.general,
|
||||
domestic: new TraitGeneralActionRouter('domestic', traitCatalog),
|
||||
war: new TraitGeneralActionRouter('war', traitCatalog),
|
||||
personality: new TraitGeneralActionRouter('personality', traitCatalog),
|
||||
crewType: crewTypeCatalog
|
||||
? (crewTypeCatalog.generalActionModule as GeneralActionModule<TriggerState>)
|
||||
: null,
|
||||
inheritance: inherit.general as GeneralActionModule<TriggerState>,
|
||||
// scenarioEffect는 현재 core runtime module이 없어 명시적으로 빈 slot입니다.
|
||||
scenario: null,
|
||||
items: items.general,
|
||||
}),
|
||||
war: createRefOrderedActionStack<WarActionModule<TriggerState>>({
|
||||
nation: new TraitWarActionRouter('nation', traitCatalog),
|
||||
officer: officer.war,
|
||||
domestic: new TraitWarActionRouter('domestic', traitCatalog),
|
||||
war: new TraitWarActionRouter('war', traitCatalog),
|
||||
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,
|
||||
items: items.war,
|
||||
}),
|
||||
itemModules,
|
||||
nationTraitModules: nation,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { RandomGenerator } from '@sammo-ts/common';
|
||||
|
||||
const generalActionEventBrand: unique symbol = Symbol('GeneralActionEvent');
|
||||
|
||||
export interface GeneralActionEventPayloadMap {
|
||||
'item.purchased': {
|
||||
itemKey: string;
|
||||
slot: 'horse' | 'weapon' | 'book' | 'item';
|
||||
};
|
||||
'item.sold': {
|
||||
itemKey: string;
|
||||
slot: 'horse' | 'weapon' | 'book' | 'item';
|
||||
};
|
||||
'strategy.succeeded': {
|
||||
consumedItems: readonly string[];
|
||||
};
|
||||
'city.conquered': {
|
||||
attacker: General;
|
||||
};
|
||||
}
|
||||
|
||||
export type GeneralActionEventType = keyof GeneralActionEventPayloadMap;
|
||||
|
||||
export type GeneralActionEventContext<
|
||||
K extends GeneralActionEventType,
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> = K extends 'item.sold' | 'city.conquered'
|
||||
? GeneralActionContext<TriggerState> & { rng: RandomGenerator } & (K extends 'item.sold'
|
||||
? { time: NonNullable<GeneralActionContext<TriggerState>['time']> }
|
||||
: object)
|
||||
: GeneralActionContext<TriggerState>;
|
||||
|
||||
/**
|
||||
* 레거시 문자열/phase/aux 삼중항을 닫힌 이벤트 계약으로 투영합니다.
|
||||
*
|
||||
* unique-symbol 필드는 런타임 분기용이 아니라 서로 다른 이벤트 payload가
|
||||
* 구조적으로 우연히 호환되는 것을 막는 nominal shadow type입니다. 이벤트는
|
||||
* 반드시 createGeneralActionEvent()로 만들며, handler는 같은 K만 반환합니다.
|
||||
*/
|
||||
export type GeneralActionEvent<
|
||||
K extends GeneralActionEventType,
|
||||
_TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> = Readonly<{
|
||||
type: K;
|
||||
payload: Readonly<GeneralActionEventPayloadMap[K]>;
|
||||
[generalActionEventBrand]: K;
|
||||
}>;
|
||||
|
||||
export type GeneralActionEventHandler<TriggerState extends GeneralTriggerState, K extends GeneralActionEventType> = {
|
||||
bivarianceHack(
|
||||
context: GeneralActionEventContext<K, TriggerState>,
|
||||
event: GeneralActionEvent<K, TriggerState>
|
||||
): GeneralActionEvent<K, TriggerState> | void;
|
||||
}['bivarianceHack'];
|
||||
|
||||
export type GeneralActionEventHandlers<TriggerState extends GeneralTriggerState = GeneralTriggerState> = Partial<{
|
||||
[K in GeneralActionEventType]: GeneralActionEventHandler<TriggerState, K>;
|
||||
}>;
|
||||
|
||||
export const createGeneralActionEvent = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
K extends GeneralActionEventType = GeneralActionEventType,
|
||||
>(
|
||||
type: K,
|
||||
payload: GeneralActionEventPayloadMap[K]
|
||||
): GeneralActionEvent<K, TriggerState> => ({
|
||||
type,
|
||||
payload,
|
||||
[generalActionEventBrand]: type,
|
||||
});
|
||||
|
||||
export const dispatchGeneralActionEventHandlers = <
|
||||
TriggerState extends GeneralTriggerState,
|
||||
K extends GeneralActionEventType,
|
||||
>(
|
||||
handlers: GeneralActionEventHandlers<TriggerState> | null | undefined,
|
||||
context: GeneralActionEventContext<K, TriggerState>,
|
||||
event: GeneralActionEvent<K, TriggerState>
|
||||
): GeneralActionEvent<K, TriggerState> => {
|
||||
// Mapped-type lookup preserves K, but TypeScript cannot currently retain
|
||||
// that correlation through a generic indexed access. This single local
|
||||
// assertion is the proof boundary; module authors and call sites remain
|
||||
// fully checked without unknown/any casts.
|
||||
const handler = handlers?.[event.type] as GeneralActionEventHandler<TriggerState, K> | undefined;
|
||||
return handler?.(context, event) ?? event;
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { type GeneralActionContext, GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type {
|
||||
GeneralStatName,
|
||||
TriggerDomesticActionType,
|
||||
TriggerDomesticVarType,
|
||||
TriggerNationalIncomeType,
|
||||
TriggerStrategicActionType,
|
||||
TriggerStrategicVarType,
|
||||
} from './types.js';
|
||||
import {
|
||||
dispatchGeneralActionEventHandlers,
|
||||
type GeneralActionEvent,
|
||||
type GeneralActionEventContext,
|
||||
type GeneralActionEventHandlers,
|
||||
type GeneralActionEventType,
|
||||
} from './events.js';
|
||||
|
||||
interface GeneralActionModuleBase<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
getName?: (() => string) | undefined;
|
||||
getInfo?: (() => string) | undefined;
|
||||
|
||||
getPreTurnExecuteTriggerList?:
|
||||
((context: GeneralActionContext<TriggerState>) => GeneralTriggerCaller<TriggerState> | null) | undefined;
|
||||
|
||||
onCalcDomestic?:
|
||||
| ((
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: TriggerDomesticActionType,
|
||||
varType: TriggerDomesticVarType,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
) => number)
|
||||
| undefined;
|
||||
|
||||
onCalcStat?:
|
||||
| ((
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
) => number)
|
||||
| undefined;
|
||||
|
||||
onCalcOpposeStat?:
|
||||
| ((
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
) => number)
|
||||
| undefined;
|
||||
|
||||
onCalcStrategic?:
|
||||
| ((
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: TriggerStrategicActionType,
|
||||
varType: TriggerStrategicVarType,
|
||||
value: number
|
||||
) => number)
|
||||
| undefined;
|
||||
|
||||
onCalcNationalIncome?:
|
||||
| ((context: GeneralActionContext<TriggerState>, type: TriggerNationalIncomeType, amount: number) => number)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface GeneralActionLeafModule<TriggerState extends GeneralTriggerState> {
|
||||
eventHandlers?: GeneralActionEventHandlers<TriggerState> | undefined;
|
||||
handleEvent?: never;
|
||||
}
|
||||
|
||||
interface GeneralActionCompositeModule<TriggerState extends GeneralTriggerState> {
|
||||
eventHandlers?: never;
|
||||
handleEvent<K extends GeneralActionEventType>(
|
||||
context: GeneralActionEventContext<K, TriggerState>,
|
||||
event: GeneralActionEvent<K, TriggerState>
|
||||
): GeneralActionEvent<K, TriggerState>;
|
||||
}
|
||||
|
||||
/**
|
||||
* leaf handler와 합성 router는 상호 배타적입니다. 한 module이 같은 이벤트를
|
||||
* 두 경로로 중복 처리할 수 없습니다.
|
||||
*/
|
||||
export type GeneralActionModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
|
||||
GeneralActionModuleBase<TriggerState> &
|
||||
(GeneralActionLeafModule<TriggerState> | GeneralActionCompositeModule<TriggerState>);
|
||||
|
||||
export const dispatchGeneralActionModuleEvent = <
|
||||
TriggerState extends GeneralTriggerState,
|
||||
K extends GeneralActionEventType,
|
||||
>(
|
||||
module: GeneralActionModule<TriggerState>,
|
||||
context: GeneralActionEventContext<K, TriggerState>,
|
||||
event: GeneralActionEvent<K, TriggerState>
|
||||
): GeneralActionEvent<K, TriggerState> =>
|
||||
module.handleEvent
|
||||
? module.handleEvent(context, event)
|
||||
: dispatchGeneralActionEventHandlers(module.eventHandlers, context, event);
|
||||
|
||||
export class GeneralActionPipeline<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly modules: ReadonlyArray<GeneralActionModule<TriggerState>>;
|
||||
|
||||
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>) {
|
||||
this.modules = modules.filter(Boolean) as ReadonlyArray<GeneralActionModule<TriggerState>>;
|
||||
}
|
||||
|
||||
getPreTurnExecuteTriggerList(context: GeneralActionContext<TriggerState>): GeneralTriggerCaller<TriggerState> {
|
||||
const triggerCaller = new GeneralTriggerCaller<TriggerState>();
|
||||
|
||||
for (const module of this.modules) {
|
||||
const triggers = module.getPreTurnExecuteTriggerList?.(context);
|
||||
if (triggers) {
|
||||
triggerCaller.merge(triggers);
|
||||
}
|
||||
}
|
||||
|
||||
return triggerCaller;
|
||||
}
|
||||
|
||||
onCalcDomestic(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: TriggerDomesticActionType,
|
||||
varType: TriggerDomesticVarType,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.modules) {
|
||||
if (!module.onCalcDomestic) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcDomestic(context, turnType, varType, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcStat(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.modules) {
|
||||
if (!module.onCalcStat) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcStat(context, statName, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcOpposeStat(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.modules) {
|
||||
if (!module.onCalcOpposeStat) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcOpposeStat(context, statName, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcStrategic(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: TriggerStrategicActionType,
|
||||
varType: TriggerStrategicVarType,
|
||||
value: number
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.modules) {
|
||||
if (!module.onCalcStrategic) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcStrategic(context, turnType, varType, current);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcNationalIncome(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
type: TriggerNationalIncomeType,
|
||||
amount: number
|
||||
): number {
|
||||
let current = amount;
|
||||
for (const module of this.modules) {
|
||||
if (!module.onCalcNationalIncome) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcNationalIncome(context, type, current);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
dispatch<K extends GeneralActionEventType>(
|
||||
context: GeneralActionEventContext<K, TriggerState>,
|
||||
event: GeneralActionEvent<K, TriggerState>
|
||||
): GeneralActionEvent<K, TriggerState> {
|
||||
let current = event;
|
||||
for (const module of this.modules) {
|
||||
current = dispatchGeneralActionModuleEvent(module, context, current);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './events.js';
|
||||
export * from './general.js';
|
||||
export * from './types.js';
|
||||
export * from './officerLevel.js';
|
||||
export * from './bundle.js';
|
||||
export * from './traits/index.js';
|
||||
@@ -0,0 +1,74 @@
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionModule } from './general.js';
|
||||
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
|
||||
const resolveOfficerLevel = (context: {
|
||||
general: { officerLevel: number; cityId: number; meta: Record<string, unknown> };
|
||||
}): number => {
|
||||
const level = context.general.officerLevel;
|
||||
if (level < 2 || level > 4) {
|
||||
return level;
|
||||
}
|
||||
const meta = asRecord(context.general.meta);
|
||||
const officerCity = meta.officerCity ?? meta.officer_city;
|
||||
return officerCity === context.general.cityId ? level : 1;
|
||||
};
|
||||
|
||||
const resolveLeadershipBonus = (officerLevel: number, nationLevel: number): number => {
|
||||
if (officerLevel === 12) {
|
||||
return nationLevel * 2;
|
||||
}
|
||||
return officerLevel >= 5 ? nationLevel : 0;
|
||||
};
|
||||
|
||||
const officerGeneralModule: GeneralActionModule = {
|
||||
onCalcDomestic: (context, turnType, varType, value) => {
|
||||
if (varType !== 'score') {
|
||||
return value;
|
||||
}
|
||||
const level = resolveOfficerLevel(context);
|
||||
if (
|
||||
((turnType === '농업' || turnType === '상업') && [12, 11, 9, 7, 5, 3].includes(level)) ||
|
||||
(turnType === '기술' && [12, 11, 9, 7, 5].includes(level)) ||
|
||||
((turnType === '민심' || turnType === '인구') && [12, 11, 2].includes(level)) ||
|
||||
((turnType === '수비' || turnType === '성벽' || turnType === '치안') &&
|
||||
[12, 11, 10, 8, 6, 4].includes(level))
|
||||
) {
|
||||
return value * 1.05;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (context, statName, value) => {
|
||||
if (statName !== 'leadership') {
|
||||
return value;
|
||||
}
|
||||
return value + resolveLeadershipBonus(resolveOfficerLevel(context), context.nation?.level ?? 0);
|
||||
},
|
||||
};
|
||||
|
||||
const officerWarModule: WarActionModule = {
|
||||
onCalcStat: (context: WarActionContext, statName, value) => {
|
||||
if (statName !== 'leadership' || typeof value !== 'number') {
|
||||
return value;
|
||||
}
|
||||
return value + resolveLeadershipBonus(resolveOfficerLevel(context), context.nation?.level ?? 0);
|
||||
},
|
||||
getWarPowerMultiplier: (context: WarActionContext) => {
|
||||
const level = resolveOfficerLevel(context);
|
||||
if (level === 12) return [1.07, 0.93];
|
||||
if (level === 11) return [1.05, 0.95];
|
||||
if ([10, 8, 6].includes(level)) return [1.1, 1];
|
||||
if ([9, 7, 5].includes(level)) return [1, 0.9];
|
||||
if ([4, 3, 2].includes(level)) return [1.05, 0.95];
|
||||
return [1, 1];
|
||||
},
|
||||
};
|
||||
|
||||
export const createOfficerLevelActionModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(): {
|
||||
general: GeneralActionModule<TriggerState>;
|
||||
war: WarActionModule<TriggerState>;
|
||||
} => ({
|
||||
general: officerGeneralModule as GeneralActionModule<TriggerState>,
|
||||
war: officerWarModule as WarActionModule<TriggerState>,
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import { dispatchGeneralActionModuleEvent, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
|
||||
import {
|
||||
type GeneralActionEvent,
|
||||
type GeneralActionEventContext,
|
||||
type GeneralActionEventType,
|
||||
} from '@sammo-ts/logic/actionModules/events.js';
|
||||
import type {
|
||||
GeneralStatName,
|
||||
TriggerDomesticActionType,
|
||||
TriggerDomesticVarType,
|
||||
TriggerNationalIncomeType,
|
||||
TriggerStrategicActionType,
|
||||
TriggerStrategicVarType,
|
||||
WarStatName,
|
||||
} from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import type { TraitCatalog, TraitKind, TraitModule } from './types.js';
|
||||
|
||||
const resolveTraitKey = (
|
||||
context: {
|
||||
general: {
|
||||
role: {
|
||||
personality: string | null;
|
||||
specialDomestic: string | null;
|
||||
specialWar: string | null;
|
||||
};
|
||||
};
|
||||
nation?: { typeCode: string } | null;
|
||||
},
|
||||
kind: TraitKind
|
||||
): string | null => {
|
||||
if (kind === 'domestic') {
|
||||
return context.general.role.specialDomestic;
|
||||
}
|
||||
if (kind === 'war') {
|
||||
return context.general.role.specialWar;
|
||||
}
|
||||
if (kind === 'nation') {
|
||||
return context.nation?.typeCode ?? null;
|
||||
}
|
||||
return context.general.role.personality;
|
||||
};
|
||||
|
||||
const resolveModule = <TriggerState extends GeneralTriggerState>(
|
||||
catalog: TraitCatalog<TriggerState>,
|
||||
kind: TraitKind,
|
||||
key: string | null
|
||||
): TraitModule<TriggerState> | null => {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
let bucket: Map<string, TraitModule<TriggerState>>;
|
||||
if (kind === 'domestic') {
|
||||
bucket = catalog.domestic;
|
||||
} else if (kind === 'war') {
|
||||
bucket = catalog.war;
|
||||
} else if (kind === 'nation') {
|
||||
bucket = catalog.nation;
|
||||
} else {
|
||||
bucket = catalog.personality;
|
||||
}
|
||||
return bucket.get(key) ?? null;
|
||||
};
|
||||
|
||||
// General 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
|
||||
export class TraitGeneralActionRouter<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
constructor(
|
||||
private readonly kind: TraitKind,
|
||||
private readonly catalog: TraitCatalog<TriggerState>
|
||||
) {}
|
||||
|
||||
private getModule(context: GeneralActionContext<TriggerState>): TraitModule<TriggerState> | null {
|
||||
const key = resolveTraitKey(context, this.kind);
|
||||
return resolveModule(this.catalog, this.kind, key);
|
||||
}
|
||||
|
||||
getPreTurnExecuteTriggerList(context: GeneralActionContext<TriggerState>) {
|
||||
const module = this.getModule(context);
|
||||
return module?.getPreTurnExecuteTriggerList?.(context) ?? null;
|
||||
}
|
||||
|
||||
onCalcDomestic(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: TriggerDomesticActionType,
|
||||
varType: TriggerDomesticVarType,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcDomestic?.(context, turnType, varType, value, aux) ?? value;
|
||||
}
|
||||
|
||||
onCalcStat(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcStat?.(context, statName, value, aux) ?? value;
|
||||
}
|
||||
|
||||
onCalcOpposeStat(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcOpposeStat?.(context, statName, value, aux) ?? value;
|
||||
}
|
||||
|
||||
onCalcStrategic(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: TriggerStrategicActionType,
|
||||
varType: TriggerStrategicVarType,
|
||||
value: number
|
||||
): number {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcStrategic?.(context, turnType, varType, value) ?? value;
|
||||
}
|
||||
|
||||
onCalcNationalIncome(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
type: TriggerNationalIncomeType,
|
||||
amount: number
|
||||
): number {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcNationalIncome?.(context, type, amount) ?? amount;
|
||||
}
|
||||
|
||||
handleEvent<K extends GeneralActionEventType>(
|
||||
context: GeneralActionEventContext<K, TriggerState>,
|
||||
event: GeneralActionEvent<K, TriggerState>
|
||||
): GeneralActionEvent<K, TriggerState> {
|
||||
const module = this.getModule(context);
|
||||
if (!module) {
|
||||
return event;
|
||||
}
|
||||
return dispatchGeneralActionModuleEvent(module, context, event);
|
||||
}
|
||||
}
|
||||
|
||||
// 전투 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
|
||||
export class TraitWarActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements WarActionModule<TriggerState> {
|
||||
constructor(
|
||||
private readonly kind: TraitKind,
|
||||
private readonly catalog: TraitCatalog<TriggerState>
|
||||
) {}
|
||||
|
||||
private getModule(context: WarActionContext<TriggerState>): TraitModule<TriggerState> | null {
|
||||
const key = resolveTraitKey(context, this.kind);
|
||||
return resolveModule(this.catalog, this.kind, key);
|
||||
}
|
||||
|
||||
getBattleInitTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
|
||||
const module = this.getModule(context);
|
||||
return module?.getBattleInitTriggerList?.(context) ?? null;
|
||||
}
|
||||
|
||||
getBattlePhaseTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
|
||||
const module = this.getModule(context);
|
||||
return module?.getBattlePhaseTriggerList?.(context) ?? null;
|
||||
}
|
||||
|
||||
onCalcStat(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcStat?.(context, statName, value, aux) ?? value;
|
||||
}
|
||||
|
||||
onCalcOpposeStat(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
const module = this.getModule(context);
|
||||
return module?.onCalcOpposeStat?.(context, statName, value, aux) ?? value;
|
||||
}
|
||||
|
||||
getWarPowerMultiplier(
|
||||
context: WarActionContext<TriggerState>,
|
||||
unit: WarUnit<TriggerState>,
|
||||
oppose: WarUnit<TriggerState>
|
||||
): [number, number] {
|
||||
const module = this.getModule(context);
|
||||
return module?.getWarPowerMultiplier?.(context, unit, oppose) ?? [1, 1];
|
||||
}
|
||||
}
|
||||
|
||||
export interface TraitModuleSet<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
general: ReadonlyArray<GeneralActionModule<TriggerState>>;
|
||||
war: WarActionModule<TriggerState>[];
|
||||
}
|
||||
|
||||
export const createTraitCatalog = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(options: {
|
||||
domestic?: TraitModule<TriggerState>[];
|
||||
war?: TraitModule<TriggerState>[];
|
||||
personality?: TraitModule<TriggerState>[];
|
||||
nation?: TraitModule<TriggerState>[];
|
||||
}): TraitCatalog<TriggerState> => {
|
||||
const domestic = new Map<string, TraitModule<TriggerState>>();
|
||||
const war = new Map<string, TraitModule<TriggerState>>();
|
||||
const personality = new Map<string, TraitModule<TriggerState>>();
|
||||
const nation = new Map<string, TraitModule<TriggerState>>();
|
||||
|
||||
const insert = (
|
||||
bucket: Map<string, TraitModule<TriggerState>>,
|
||||
kind: TraitKind,
|
||||
module: TraitModule<TriggerState>
|
||||
): void => {
|
||||
if (module.kind !== kind) {
|
||||
throw new Error(`Trait ${module.key} declared kind ${module.kind}, expected ${kind}`);
|
||||
}
|
||||
if (bucket.has(module.key)) {
|
||||
throw new Error(`Duplicate ${kind} trait key: ${module.key}`);
|
||||
}
|
||||
bucket.set(module.key, module);
|
||||
};
|
||||
|
||||
for (const module of options.domestic ?? []) {
|
||||
insert(domestic, 'domestic', module);
|
||||
}
|
||||
for (const module of options.war ?? []) {
|
||||
insert(war, 'war', module);
|
||||
}
|
||||
for (const module of options.personality ?? []) {
|
||||
insert(personality, 'personality', module);
|
||||
}
|
||||
for (const module of options.nation ?? []) {
|
||||
insert(nation, 'nation', module);
|
||||
}
|
||||
|
||||
return { domestic, war, personality, nation };
|
||||
};
|
||||
|
||||
// 특성 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다.
|
||||
export const createTraitModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
|
||||
catalog: TraitCatalog<TriggerState>
|
||||
): TraitModuleSet<TriggerState> => ({
|
||||
general: [
|
||||
new TraitGeneralActionRouter<TriggerState>('domestic', catalog),
|
||||
new TraitGeneralActionRouter<TriggerState>('war', catalog),
|
||||
new TraitGeneralActionRouter<TriggerState>('personality', catalog),
|
||||
new TraitGeneralActionRouter<TriggerState>('nation', catalog),
|
||||
],
|
||||
war: [
|
||||
new TraitWarActionRouter<TriggerState>('domestic', catalog),
|
||||
new TraitWarActionRouter<TriggerState>('war', catalog),
|
||||
new TraitWarActionRouter<TriggerState>('personality', catalog),
|
||||
new TraitWarActionRouter<TriggerState>('nation', catalog),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 경작
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_경작',
|
||||
name: '경작',
|
||||
info: '[내정] 농지 개간 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '경작',
|
||||
getInfo: () => '[내정] 농지 개간 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '농업') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 귀모
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_귀모',
|
||||
name: '귀모',
|
||||
info: '[계략] 화계·탈취·파괴·선동 : 성공률 +20%p',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
|
||||
weight: 2.5,
|
||||
weightType: TraitWeightType.PERCENT,
|
||||
},
|
||||
getName: () => '귀모',
|
||||
getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +20%p',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '계략') {
|
||||
if (varType === 'success') {
|
||||
return value + 0.2;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 발명
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_발명',
|
||||
name: '발명',
|
||||
info: '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '발명',
|
||||
getInfo: () => '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '기술') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 상재
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_상재',
|
||||
name: '상재',
|
||||
info: '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '상재',
|
||||
getInfo: () => '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '상업') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 수비
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_수비',
|
||||
name: '수비',
|
||||
info: '[내정] 수비 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '수비',
|
||||
getInfo: () => '[내정] 수비 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '수비') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 인덕
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_인덕',
|
||||
name: '인덕',
|
||||
info: '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '인덕',
|
||||
getInfo: () => '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 축성
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_축성',
|
||||
name: '축성',
|
||||
info: '[내정] 성벽 보수 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '축성',
|
||||
getInfo: () => '[내정] 성벽 보수 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '성벽') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 내정 특기: 통찰
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_통찰',
|
||||
name: '통찰',
|
||||
info: '[내정] 치안 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
kind: 'domestic',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getName: () => '통찰',
|
||||
getInfo: () => '[내정] 치안 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '치안') {
|
||||
if (varType === 'score') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
if (varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
export const DOMESTIC_TRAIT_KEYS = [
|
||||
'che_인덕',
|
||||
'che_발명',
|
||||
'che_경작',
|
||||
'che_상재',
|
||||
'che_축성',
|
||||
'che_수비',
|
||||
'che_통찰',
|
||||
'che_귀모',
|
||||
] as const;
|
||||
|
||||
export type DomesticTraitKey = (typeof DOMESTIC_TRAIT_KEYS)[number];
|
||||
|
||||
export type DomesticTraitModule = TraitModule;
|
||||
|
||||
export type DomesticTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<DomesticTraitKey, DomesticTraitImporter> = {
|
||||
che_인덕: async () => import('./che_인덕.js'),
|
||||
che_발명: async () => import('./che_발명.js'),
|
||||
che_경작: async () => import('./che_경작.js'),
|
||||
che_상재: async () => import('./che_상재.js'),
|
||||
che_축성: async () => import('./che_축성.js'),
|
||||
che_수비: async () => import('./che_수비.js'),
|
||||
che_통찰: async () => import('./che_통찰.js'),
|
||||
che_귀모: async () => import('./che_귀모.js'),
|
||||
};
|
||||
|
||||
export const isDomesticTraitKey = (value: string): value is DomesticTraitKey =>
|
||||
DOMESTIC_TRAIT_KEYS.includes(value as DomesticTraitKey);
|
||||
|
||||
export class DomesticTraitLoader {
|
||||
private readonly cache = new Map<DomesticTraitKey, Promise<DomesticTraitModule>>();
|
||||
|
||||
constructor(private readonly importers: Record<DomesticTraitKey, DomesticTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: DomesticTraitKey): Promise<DomesticTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const importer = this.importers[key];
|
||||
if (!importer) {
|
||||
throw new Error(`Unknown domestic trait key: ${key}`);
|
||||
}
|
||||
const loading = importer().then((module) => {
|
||||
if (!('traitModule' in module)) {
|
||||
throw new Error(`Missing traitModule for domestic trait: ${key}`);
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(`Domestic trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'domestic') {
|
||||
throw new Error(`Domestic trait kind mismatch: ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
this.cache.set(key, loading);
|
||||
return loading;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadDomesticTraitModules = async (
|
||||
keys: DomesticTraitKey[],
|
||||
loader: DomesticTraitLoader = new DomesticTraitLoader()
|
||||
): Promise<DomesticTraitModule[]> => {
|
||||
const modules: DomesticTraitModule[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const key of keys) {
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
modules.push(await loader.load(key));
|
||||
}
|
||||
return modules;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './types.js';
|
||||
export * from './requirements.js';
|
||||
export * from './selector.js';
|
||||
export * from './catalog.js';
|
||||
export * from './domestic/index.js';
|
||||
export * from './war/index.js';
|
||||
export * from './personality/index.js';
|
||||
export * from './nation/index.js';
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 덕가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_덕가',
|
||||
name: '덕가',
|
||||
info: '치안↑ 인구↑ 민심↑ 쌀수입↓ 수성↓',
|
||||
kind: 'nation',
|
||||
getName: () => '덕가',
|
||||
getInfo: () => '치안↑ 인구↑ 민심↑ 쌀수입↓ 수성↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '치안' || turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'rice') return amount * 0.9;
|
||||
if (type === 'pop' && amount > 0) return amount * 1.2;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 도가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_도가',
|
||||
name: '도가',
|
||||
info: '인구↑ 기술↓ 치안↓',
|
||||
kind: 'nation',
|
||||
getName: () => '도가',
|
||||
getInfo: () => '인구↑ 기술↓ 치안↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '기술' || turnType === '치안') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'pop' && amount > 0) return amount * 1.2;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 도적
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_도적',
|
||||
name: '도적',
|
||||
info: '계략↑ 금수입↓ 치안↓ 민심↓',
|
||||
kind: 'nation',
|
||||
getName: () => '도적',
|
||||
getInfo: () => '계략↑ 금수입↓ 치안↓ 민심↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '치안' || turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
} else if (turnType === '계략') {
|
||||
if (varType === 'success') return value + 0.1;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'gold') return amount * 0.9;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 명가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_명가',
|
||||
name: '명가',
|
||||
info: '기술↑ 인구↑ 쌀수입↓ 수성↓',
|
||||
kind: 'nation',
|
||||
getName: () => '명가',
|
||||
getInfo: () => '기술↑ 인구↑ 쌀수입↓ 수성↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '기술') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'rice') return amount * 0.9;
|
||||
if (type === 'pop' && amount > 0) return amount * 1.2;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 묵가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_묵가',
|
||||
name: '묵가',
|
||||
info: '수성↑ 기술↓',
|
||||
kind: 'nation',
|
||||
getName: () => '묵가',
|
||||
getInfo: () => '수성↑ 기술↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '기술') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 법가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_법가',
|
||||
name: '법가',
|
||||
info: '금수입↑ 치안↑ 인구↓ 민심↓',
|
||||
kind: 'nation',
|
||||
getName: () => '법가',
|
||||
getInfo: () => '금수입↑ 치안↑ 인구↓ 민심↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '치안') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'gold') return amount * 1.1;
|
||||
if (type === 'pop' && amount > 0) return amount * 0.8;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 병가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_병가',
|
||||
name: '병가',
|
||||
info: '기술↑ 수성↑ 인구↓ 민심↓',
|
||||
kind: 'nation',
|
||||
getName: () => '병가',
|
||||
getInfo: () => '기술↑ 수성↑ 인구↓ 민심↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '기술' || turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'pop' && amount > 0) return amount * 0.8;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 불가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_불가',
|
||||
name: '불가',
|
||||
info: '민심↑ 수성↑ 금수입↓',
|
||||
kind: 'nation',
|
||||
getName: () => '불가',
|
||||
getInfo: () => '민심↑ 수성↑ 금수입↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '민심' || turnType === '인구' || turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'gold') return amount * 0.9;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 오두미도
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_오두미도',
|
||||
name: '오두미도',
|
||||
info: '쌀수입↑ 인구↑ 기술↓ 수성↓ 농상↓',
|
||||
kind: 'nation',
|
||||
getName: () => '오두미도',
|
||||
getInfo: () => '쌀수입↑ 인구↑ 기술↓ 수성↓ 농상↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (
|
||||
turnType === '기술' ||
|
||||
turnType === '수비' ||
|
||||
turnType === '성벽' ||
|
||||
turnType === '농업' ||
|
||||
turnType === '상업'
|
||||
) {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'rice') return amount * 1.1;
|
||||
if (type === 'pop' && amount > 0) return amount * 1.2;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 유가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_유가',
|
||||
name: '유가',
|
||||
info: '농상↑ 민심↑ 쌀수입↓',
|
||||
kind: 'nation',
|
||||
getName: () => '유가',
|
||||
getInfo: () => '농상↑ 민심↑ 쌀수입↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '농업' || turnType === '상업' || turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'rice') return amount * 0.9;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 음양가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_음양가',
|
||||
name: '음양가',
|
||||
info: '농상↑ 인구↑ 기술↓ 전략↓',
|
||||
kind: 'nation',
|
||||
getName: () => '음양가',
|
||||
getInfo: () => '농상↑ 인구↑ 기술↓ 전략↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '농업' || turnType === '상업') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '기술') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'pop' && amount > 0) return amount * 1.2;
|
||||
return amount;
|
||||
},
|
||||
onCalcStrategic: (_context, _turnType, varType, value) => {
|
||||
if (varType === 'delay') {
|
||||
return Math.round((value * 4) / 3);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 종횡가
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_종횡가',
|
||||
name: '종횡가',
|
||||
info: '전략↑ 수성↑ 금수입↓ 농상↓',
|
||||
kind: 'nation',
|
||||
getName: () => '종횡가',
|
||||
getInfo: () => '전략↑ 수성↑ 금수입↓ 농상↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '농업' || turnType === '상업') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'gold') return amount * 0.9;
|
||||
return amount;
|
||||
},
|
||||
onCalcStrategic: (_context, _turnType, varType, value) => {
|
||||
if (varType === 'delay') {
|
||||
return Math.round((value * 3) / 4);
|
||||
}
|
||||
if (varType === 'globalDelay') {
|
||||
return Math.round(value / 2);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 중립
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_중립',
|
||||
name: '-',
|
||||
info: ' ',
|
||||
kind: 'nation',
|
||||
getName: () => '-',
|
||||
getInfo: () => ' ',
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
// 국가 성향: 태평도
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_태평도',
|
||||
name: '태평도',
|
||||
info: '인구↑ 민심↑ 기술↓ 수성↓',
|
||||
kind: 'nation',
|
||||
getName: () => '태평도',
|
||||
getInfo: () => '인구↑ 민심↑ 기술↓ 수성↓',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '민심' || turnType === '인구') {
|
||||
if (varType === 'score') return value * 1.1;
|
||||
if (varType === 'cost') return value * 0.8;
|
||||
} else if (turnType === '기술' || turnType === '수비' || turnType === '성벽') {
|
||||
if (varType === 'score') return value * 0.9;
|
||||
if (varType === 'cost') return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcNationalIncome: (_context, type, amount) => {
|
||||
if (type === 'pop' && amount > 0) return amount * 1.2;
|
||||
return amount;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
export const NATION_TRAIT_KEYS = [
|
||||
'che_중립',
|
||||
'che_도적',
|
||||
'che_명가',
|
||||
'che_음양가',
|
||||
'che_종횡가',
|
||||
'che_불가',
|
||||
'che_오두미도',
|
||||
'che_태평도',
|
||||
'che_도가',
|
||||
'che_묵가',
|
||||
'che_덕가',
|
||||
'che_병가',
|
||||
'che_유가',
|
||||
'che_법가',
|
||||
] as const;
|
||||
|
||||
export type NationTraitKey = (typeof NATION_TRAIT_KEYS)[number];
|
||||
|
||||
export type NationTraitModule = TraitModule;
|
||||
|
||||
export type NationTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<NationTraitKey, NationTraitImporter> = {
|
||||
che_중립: async () => import('./che_중립.js'),
|
||||
che_도적: async () => import('./che_도적.js'),
|
||||
che_명가: async () => import('./che_명가.js'),
|
||||
che_음양가: async () => import('./che_음양가.js'),
|
||||
che_종횡가: async () => import('./che_종횡가.js'),
|
||||
che_불가: async () => import('./che_불가.js'),
|
||||
che_오두미도: async () => import('./che_오두미도.js'),
|
||||
che_태평도: async () => import('./che_태평도.js'),
|
||||
che_도가: async () => import('./che_도가.js'),
|
||||
che_묵가: async () => import('./che_묵가.js'),
|
||||
che_덕가: async () => import('./che_덕가.js'),
|
||||
che_병가: async () => import('./che_병가.js'),
|
||||
che_유가: async () => import('./che_유가.js'),
|
||||
che_법가: async () => import('./che_법가.js'),
|
||||
};
|
||||
|
||||
export const isNationTraitKey = (value: string): value is NationTraitKey =>
|
||||
NATION_TRAIT_KEYS.includes(value as NationTraitKey);
|
||||
|
||||
export class NationTraitLoader {
|
||||
private readonly cache = new Map<NationTraitKey, Promise<NationTraitModule>>();
|
||||
|
||||
constructor(private readonly importers: Record<NationTraitKey, NationTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: NationTraitKey): Promise<NationTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const importer = this.importers[key];
|
||||
if (!importer) {
|
||||
throw new Error(`Unknown nation trait key: ${key}`);
|
||||
}
|
||||
const loading = importer().then((module) => {
|
||||
if (!('traitModule' in module)) {
|
||||
throw new Error(`Missing traitModule for nation trait: ${key}`);
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(`Nation trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'nation') {
|
||||
throw new Error(`Nation trait kind mismatch: ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
this.cache.set(key, loading);
|
||||
return loading;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadNationTraitModules = async (
|
||||
keys: NationTraitKey[],
|
||||
loader: NationTraitLoader = new NationTraitLoader()
|
||||
): Promise<NationTraitModule[]> => {
|
||||
const modules: NationTraitModule[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const key of keys) {
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
modules.push(await loader.load(key));
|
||||
}
|
||||
return modules;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_대의',
|
||||
name: '대의',
|
||||
info: '명성 +10%, 훈련 -5',
|
||||
kind: 'personality',
|
||||
getName: () => '대의',
|
||||
getInfo: () => '명성 +10%, 훈련 -5',
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (typeof value === 'number') {
|
||||
if (statName === 'experience') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (statName === 'bonusTrain') {
|
||||
return value - 5;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_안전',
|
||||
name: '안전',
|
||||
info: '사기 -5, 징·모병 비용 -20%',
|
||||
kind: 'personality',
|
||||
getName: () => '안전',
|
||||
getInfo: () => '사기 -5, 징·모병 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if ((turnType === '징병' || turnType === '모병') && varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName === 'bonusAtmos' && typeof value === 'number') {
|
||||
return value - 5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_왕좌',
|
||||
name: '왕좌',
|
||||
info: '명성 +10%, 사기 -5',
|
||||
kind: 'personality',
|
||||
getName: () => '왕좌',
|
||||
getInfo: () => '명성 +10%, 사기 -5',
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (typeof value === 'number') {
|
||||
if (statName === 'experience') {
|
||||
return value * 1.1;
|
||||
}
|
||||
if (statName === 'bonusAtmos') {
|
||||
return value - 5;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_유지',
|
||||
name: '유지',
|
||||
info: '훈련 -5, 징·모병 비용 -20%',
|
||||
kind: 'personality',
|
||||
getName: () => '유지',
|
||||
getInfo: () => '훈련 -5, 징·모병 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if ((turnType === '징병' || turnType === '모병') && varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName === 'bonusTrain' && typeof value === 'number') {
|
||||
return value - 5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_은둔',
|
||||
name: '은둔',
|
||||
info: '명성 -10%, 계급 -10%, 사기 -5, 훈련 -5, 단련 성공률 +10%',
|
||||
kind: 'personality',
|
||||
getName: () => '은둔',
|
||||
getInfo: () => '명성 -10%, 계급 -10%, 사기 -5, 훈련 -5, 단련 성공률 +10%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '단련' && varType === 'success') {
|
||||
return value + 0.1;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (typeof value === 'number') {
|
||||
if (statName === 'experience') {
|
||||
return value * 0.9;
|
||||
}
|
||||
if (statName === 'dedication') {
|
||||
return value * 0.9;
|
||||
}
|
||||
if (statName === 'bonusAtmos') {
|
||||
return value - 5;
|
||||
}
|
||||
if (statName === 'bonusTrain') {
|
||||
return value - 5;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_의협',
|
||||
name: '의협',
|
||||
info: '사기 +5, 징·모병 비용 +20%',
|
||||
kind: 'personality',
|
||||
getName: () => '의협',
|
||||
getInfo: () => '사기 +5, 징·모병 비용 +20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if ((turnType === '징병' || turnType === '모병') && varType === 'cost') {
|
||||
return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName === 'bonusAtmos' && typeof value === 'number') {
|
||||
return value + 5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_재간',
|
||||
name: '재간',
|
||||
info: '명성 -10%, 징·모병 비용 -20%',
|
||||
kind: 'personality',
|
||||
getName: () => '재간',
|
||||
getInfo: () => '명성 -10%, 징·모병 비용 -20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if ((turnType === '징병' || turnType === '모병') && varType === 'cost') {
|
||||
return value * 0.8;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName === 'experience' && typeof value === 'number') {
|
||||
return value * 0.9;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_정복',
|
||||
name: '정복',
|
||||
info: '명성 -10%, 사기 +5',
|
||||
kind: 'personality',
|
||||
getName: () => '정복',
|
||||
getInfo: () => '명성 -10%, 사기 +5',
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (typeof value === 'number') {
|
||||
if (statName === 'experience') {
|
||||
return value * 0.9;
|
||||
}
|
||||
if (statName === 'bonusAtmos') {
|
||||
return value + 5;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_출세',
|
||||
name: '출세',
|
||||
info: '명성 +10%, 징·모병 비용 +20%',
|
||||
kind: 'personality',
|
||||
getName: () => '출세',
|
||||
getInfo: () => '명성 +10%, 징·모병 비용 +20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if ((turnType === '징병' || turnType === '모병') && varType === 'cost') {
|
||||
return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName === 'experience' && typeof value === 'number') {
|
||||
return value * 1.1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_패권',
|
||||
name: '패권',
|
||||
info: '훈련 +5, 징·모병 비용 +20%',
|
||||
kind: 'personality',
|
||||
getName: () => '패권',
|
||||
getInfo: () => '훈련 +5, 징·모병 비용 +20%',
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if ((turnType === '징병' || turnType === '모병') && varType === 'cost') {
|
||||
return value * 1.2;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName === 'bonusTrain' && typeof value === 'number') {
|
||||
return value + 5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_할거',
|
||||
name: '할거',
|
||||
info: '명성 -10%, 훈련 +5',
|
||||
kind: 'personality',
|
||||
getName: () => '할거',
|
||||
getInfo: () => '명성 -10%, 훈련 +5',
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: GeneralStatName,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (typeof value === 'number') {
|
||||
if (statName === 'experience') {
|
||||
return value * 0.9;
|
||||
}
|
||||
if (statName === 'bonusTrain') {
|
||||
return value + 5;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<TraitModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
export const PERSONALITY_TRAIT_KEYS = [
|
||||
'che_안전',
|
||||
'che_유지',
|
||||
'che_재간',
|
||||
'che_출세',
|
||||
'che_할거',
|
||||
'che_정복',
|
||||
'che_패권',
|
||||
'che_의협',
|
||||
'che_대의',
|
||||
'che_왕좌',
|
||||
'che_은둔',
|
||||
] as const;
|
||||
|
||||
export type PersonalityTraitKey = (typeof PERSONALITY_TRAIT_KEYS)[number];
|
||||
|
||||
export type PersonalityTraitModule = TraitModule;
|
||||
|
||||
export type PersonalityTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<PersonalityTraitKey, PersonalityTraitImporter> = {
|
||||
che_안전: async () => import('./che_안전.js'),
|
||||
che_유지: async () => import('./che_유지.js'),
|
||||
che_재간: async () => import('./che_재간.js'),
|
||||
che_출세: async () => import('./che_출세.js'),
|
||||
che_할거: async () => import('./che_할거.js'),
|
||||
che_정복: async () => import('./che_정복.js'),
|
||||
che_패권: async () => import('./che_패권.js'),
|
||||
che_의협: async () => import('./che_의협.js'),
|
||||
che_대의: async () => import('./che_대의.js'),
|
||||
che_왕좌: async () => import('./che_왕좌.js'),
|
||||
che_은둔: async () => import('./che_은둔.js'),
|
||||
};
|
||||
|
||||
export const isPersonalityTraitKey = (value: string): value is PersonalityTraitKey =>
|
||||
PERSONALITY_TRAIT_KEYS.includes(value as PersonalityTraitKey);
|
||||
|
||||
export class PersonalityTraitLoader {
|
||||
private readonly cache = new Map<PersonalityTraitKey, Promise<PersonalityTraitModule>>();
|
||||
|
||||
constructor(private readonly importers: Record<PersonalityTraitKey, PersonalityTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: PersonalityTraitKey): Promise<PersonalityTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const importer = this.importers[key];
|
||||
if (!importer) {
|
||||
throw new Error(`Unknown personality trait key: ${key}`);
|
||||
}
|
||||
const loading = importer().then((module) => {
|
||||
if (!('traitModule' in module)) {
|
||||
throw new Error(`Missing traitModule for personality trait: ${key}`);
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(`Personality trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'personality') {
|
||||
throw new Error(`Personality trait kind mismatch: ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
this.cache.set(key, loading);
|
||||
return loading;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadPersonalityTraitModules = async (
|
||||
keys: PersonalityTraitKey[],
|
||||
loader: PersonalityTraitLoader = new PersonalityTraitLoader()
|
||||
): Promise<PersonalityTraitModule[]> => {
|
||||
const modules: PersonalityTraitModule[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const key of keys) {
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
modules.push(await loader.load(key));
|
||||
}
|
||||
return modules;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export enum TraitRequirement {
|
||||
DISABLED = 0x1,
|
||||
|
||||
STAT_LEADERSHIP = 0x2,
|
||||
STAT_STRENGTH = 0x4,
|
||||
STAT_INTEL = 0x8,
|
||||
|
||||
ARMY_FOOTMAN = 0x100,
|
||||
ARMY_ARCHER = 0x200,
|
||||
ARMY_CAVALRY = 0x400,
|
||||
ARMY_WIZARD = 0x800,
|
||||
ARMY_SIEGE = 0x1000,
|
||||
|
||||
REQ_DEXTERITY = 0x4000,
|
||||
|
||||
STAT_NOT_LEADERSHIP = 0x20000,
|
||||
STAT_NOT_STRENGTH = 0x40000,
|
||||
STAT_NOT_INTEL = 0x80000,
|
||||
}
|
||||
|
||||
export enum TraitWeightType {
|
||||
NORM = 1,
|
||||
PERCENT = 2,
|
||||
}
|
||||
|
||||
export interface TraitSelection {
|
||||
weightType: TraitWeightType;
|
||||
weight: number;
|
||||
requirements: TraitRequirement[];
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import type { RandUtil } from '@sammo-ts/common';
|
||||
import { TraitRequirement, TraitWeightType } from './requirements.js';
|
||||
import type { TraitModule } from './types.js';
|
||||
import type { ScenarioStatBlock } from '../../scenario/types.js';
|
||||
|
||||
export class TraitSelector {
|
||||
/**
|
||||
* 기초 스탯 기반 선택 조건 계산 (calcCondGeneric)
|
||||
*/
|
||||
static calcCondGeneric(
|
||||
stats: { leadership: number; strength: number; intelligence: number },
|
||||
scenarioStat: ScenarioStatBlock
|
||||
): number {
|
||||
const { leadership, strength, intelligence } = stats;
|
||||
const chiefMin = scenarioStat.chiefMin;
|
||||
|
||||
let myCond = 0;
|
||||
if (leadership > chiefMin) {
|
||||
myCond |= TraitRequirement.STAT_LEADERSHIP;
|
||||
}
|
||||
if (strength >= intelligence * 0.95 && strength > chiefMin) {
|
||||
myCond |= TraitRequirement.STAT_STRENGTH;
|
||||
}
|
||||
if (intelligence >= strength * 0.95 && intelligence > chiefMin) {
|
||||
myCond |= TraitRequirement.STAT_INTEL;
|
||||
}
|
||||
|
||||
if (myCond !== 0) {
|
||||
if (leadership < chiefMin) myCond |= TraitRequirement.STAT_NOT_LEADERSHIP;
|
||||
if (strength < chiefMin) myCond |= TraitRequirement.STAT_NOT_STRENGTH;
|
||||
if (intelligence < chiefMin) myCond |= TraitRequirement.STAT_NOT_INTEL;
|
||||
}
|
||||
|
||||
if (myCond === 0) {
|
||||
if (leadership * 0.9 > strength && leadership * 0.9 > intelligence) {
|
||||
myCond |= TraitRequirement.STAT_LEADERSHIP;
|
||||
} else if (strength >= intelligence) {
|
||||
myCond |= TraitRequirement.STAT_STRENGTH;
|
||||
} else {
|
||||
myCond |= TraitRequirement.STAT_INTEL;
|
||||
}
|
||||
}
|
||||
|
||||
return myCond;
|
||||
}
|
||||
|
||||
/**
|
||||
* 숙련도 기반 선택 조건 계산 (calcCondDexterity)
|
||||
*/
|
||||
static calcCondDexterity(rng: RandUtil, dex: number[]): number {
|
||||
const dexMap: Record<number, number> = {
|
||||
[TraitRequirement.ARMY_FOOTMAN]: dex[0] || 0,
|
||||
[TraitRequirement.ARMY_ARCHER]: dex[1] || 0,
|
||||
[TraitRequirement.ARMY_CAVALRY]: dex[2] || 0,
|
||||
[TraitRequirement.ARMY_WIZARD]: dex[3] || 0,
|
||||
[TraitRequirement.ARMY_SIEGE]: dex[4] || 0,
|
||||
};
|
||||
|
||||
const dexSum = Object.values(dexMap).reduce((a, b) => a + b, 0);
|
||||
// 루트(합)/4 확률 기반 로직 (Legacy: sqrt(dexSum)/4)
|
||||
const dexBase = Math.round(Math.sqrt(dexSum) / 4);
|
||||
|
||||
// Legacy: 80% 확률로 0 반환 (이전 연도에 이미 얻었거나 기타 이유로 제한하는 인지)
|
||||
// 실제로는 pickSpecialWar에서 이 메서드 호출 전후에 별도 확률을 둘 수도 있으나,
|
||||
// Legacy SpecialityHelper.php의 로직을 그대로 따름.
|
||||
if (rng.nextBool(0.8)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (rng.nextRangeInt(0, 99) < dexBase) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (dexSum === 0) {
|
||||
return rng.choice(Object.values(dexMap));
|
||||
}
|
||||
|
||||
const maxDex = Math.max(...Object.values(dexMap));
|
||||
const candidates = Object.keys(dexMap)
|
||||
.map(Number)
|
||||
.filter((k) => dexMap[k] === maxDex);
|
||||
|
||||
return Number(rng.choice(candidates));
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용 가능한 특기 목록에서 하나를 무작위로 선택 (pickTrait)
|
||||
*/
|
||||
private static pickTraitOnce(
|
||||
rng: RandUtil,
|
||||
myCond: number,
|
||||
traits: TraitModule[],
|
||||
prevTraitKeys: string[],
|
||||
preferDexterity: boolean
|
||||
): string | null {
|
||||
const dexterityPool: Array<[string, number]> = [];
|
||||
const normPool: Array<[string, number]> = [];
|
||||
const percentPool: Array<[string | null, number]> = [];
|
||||
|
||||
for (const trait of traits) {
|
||||
if (!trait.selection) continue;
|
||||
if (prevTraitKeys.includes(trait.key)) continue;
|
||||
|
||||
let matchedRequirement: number | null = null;
|
||||
for (const req of trait.selection.requirements) {
|
||||
if (req === (req & myCond)) {
|
||||
matchedRequirement = req;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedRequirement === null) continue;
|
||||
|
||||
if (preferDexterity && (matchedRequirement & TraitRequirement.REQ_DEXTERITY) !== 0) {
|
||||
dexterityPool.push([trait.key, trait.selection.weight]);
|
||||
} else if (trait.selection.weightType === TraitWeightType.PERCENT) {
|
||||
percentPool.push([trait.key, trait.selection.weight]);
|
||||
} else {
|
||||
normPool.push([trait.key, trait.selection.weight]);
|
||||
}
|
||||
}
|
||||
|
||||
if (dexterityPool.length > 0) {
|
||||
return rng.choiceUsingWeightPair(dexterityPool);
|
||||
}
|
||||
|
||||
if (percentPool.length > 0) {
|
||||
if (normPool.length > 0) {
|
||||
const totalPercent = percentPool.reduce((sum, [, weight]) => sum + weight, 0);
|
||||
percentPool.push([null, Math.max(0, 100 - totalPercent)]);
|
||||
}
|
||||
const selected = rng.choiceUsingWeightPair(percentPool);
|
||||
if (selected !== null) {
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
|
||||
if (normPool.length > 0) {
|
||||
return rng.choiceUsingWeightPair(normPool);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 전투 특기 선택 통합 로직
|
||||
*/
|
||||
static pickWarTrait(
|
||||
rng: RandUtil,
|
||||
stats: { leadership: number; strength: number; intelligence: number },
|
||||
dex: number[],
|
||||
traits: TraitModule[],
|
||||
prevTraitKeys: string[],
|
||||
scenarioStat: ScenarioStatBlock
|
||||
): string | null {
|
||||
const myCond =
|
||||
this.calcCondGeneric(stats, scenarioStat) |
|
||||
this.calcCondDexterity(rng, dex) |
|
||||
TraitRequirement.REQ_DEXTERITY;
|
||||
const selected = this.pickTraitOnce(rng, myCond, traits, prevTraitKeys, true);
|
||||
if (selected !== null) {
|
||||
return selected;
|
||||
}
|
||||
if (prevTraitKeys.length > 0) {
|
||||
return this.pickWarTrait(rng, stats, dex, traits, [], scenarioStat);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 내정 특기 선택 통합 로직
|
||||
*/
|
||||
static pickDomesticTrait(
|
||||
rng: RandUtil,
|
||||
stats: { leadership: number; strength: number; intelligence: number },
|
||||
traits: TraitModule[],
|
||||
prevTraitKeys: string[],
|
||||
scenarioStat: ScenarioStatBlock
|
||||
): string | null {
|
||||
const myCond = this.calcCondGeneric(stats, scenarioStat);
|
||||
const selected = this.pickTraitOnce(rng, myCond, traits, prevTraitKeys, false);
|
||||
if (selected !== null) {
|
||||
return selected;
|
||||
}
|
||||
if (prevTraitKeys.length > 0) {
|
||||
return this.pickDomesticTrait(rng, stats, traits, [], scenarioStat);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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 type { TraitSelection } from './requirements.js';
|
||||
|
||||
export type TraitKind = 'domestic' | 'war' | 'personality' | 'nation';
|
||||
|
||||
export interface TraitSpec {
|
||||
key: string;
|
||||
name: string;
|
||||
info: string;
|
||||
kind: TraitKind;
|
||||
selection?: TraitSelection;
|
||||
}
|
||||
|
||||
export type TraitModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> = TraitSpec &
|
||||
GeneralActionModule<TriggerState> &
|
||||
WarActionModule<TriggerState>;
|
||||
|
||||
export interface TraitModuleExport<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
traitModule: TraitModule<TriggerState>;
|
||||
}
|
||||
|
||||
export interface TraitCatalog<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
domestic: Map<string, TraitModule<TriggerState>>;
|
||||
war: Map<string, TraitModule<TriggerState>>;
|
||||
personality: Map<string, TraitModule<TriggerState>>;
|
||||
nation: Map<string, TraitModule<TriggerState>>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
export interface WarDexAux {
|
||||
isAttacker?: boolean;
|
||||
opposeType?: { armType: number };
|
||||
}
|
||||
|
||||
export const parseWarDexAux = (aux: unknown): WarDexAux => {
|
||||
if (!isRecord(aux)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isAttacker = typeof aux.isAttacker === 'boolean' ? aux.isAttacker : undefined;
|
||||
const opposeRaw = aux.opposeType;
|
||||
|
||||
if (!isRecord(opposeRaw)) {
|
||||
return isAttacker === undefined ? {} : { isAttacker };
|
||||
}
|
||||
|
||||
const armType = opposeRaw.armType;
|
||||
if (typeof armType !== 'number') {
|
||||
return isAttacker === undefined ? {} : { isAttacker };
|
||||
}
|
||||
|
||||
const opposeType = { armType };
|
||||
return isAttacker === undefined ? { opposeType } : { isAttacker, opposeType };
|
||||
};
|
||||
|
||||
export const getAuxArmType = (aux: unknown): number | undefined => {
|
||||
if (!isRecord(aux)) {
|
||||
return undefined;
|
||||
}
|
||||
const armType = aux.armType;
|
||||
return typeof armType === 'number' ? armType : undefined;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_격노발동, che_격노시도 } from '@sammo-ts/logic/war/triggers/che_격노.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
_statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_격노',
|
||||
name: '격노',
|
||||
info: '[전투] 상대방 필살 시 격노(필살) 발동, 회피 시도시 25% 확률로 격노 발동, 공격 시 일정 확률로 진노(1페이즈 추가), 격노마다 대미지 20% 추가 중첩',
|
||||
kind: 'war',
|
||||
getName: () => '격노',
|
||||
getInfo: () =>
|
||||
'[전투] 상대방 필살 시 격노(필살) 발동, 회피 시도시 25% 확률로 격노 발동, 공격 시 일정 확률로 진노(1페이즈 추가), 격노마다 대미지 20% 추가 중첩',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getWarPowerMultiplier: (_context, unit, _oppose) => {
|
||||
const activatedCnt = unit.hasActivatedSkillOnLog('격노');
|
||||
return [1 + 0.2 * activatedCnt, 1];
|
||||
},
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_격노시도(_context.unit), new che_격노발동(_context.unit));
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_부상무효 } from '@sammo-ts/logic/war/triggers/che_견고.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
_statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_견고',
|
||||
name: '견고',
|
||||
info: '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%',
|
||||
kind: 'war',
|
||||
getName: () => '견고',
|
||||
getInfo: () => '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
onCalcOpposeStat: ((_context, statName, value, _aux) => {
|
||||
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
|
||||
return value - 0.1;
|
||||
}
|
||||
if (statName === 'warCriticalRatio' && typeof value === 'number') {
|
||||
return value - 0.2;
|
||||
}
|
||||
return value;
|
||||
}) as TraitModule['onCalcOpposeStat'],
|
||||
getBattleInitTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_부상무효(_context.unit));
|
||||
},
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_부상무효(_context.unit));
|
||||
},
|
||||
getWarPowerMultiplier: (_context, _unit, _oppose) => {
|
||||
return [1, 0.9];
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
import { WarUnitCity } from '@sammo-ts/logic/war/units.js';
|
||||
import { getAuxArmType, parseWarDexAux } from './aux.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (!('unit' in context) || !context.unit) {
|
||||
return value;
|
||||
}
|
||||
const unit = context.unit;
|
||||
|
||||
const siegeType = unit.getGameConfig().armTypes.siege;
|
||||
if (siegeType === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (statName.startsWith('dex')) {
|
||||
const myDex = getMetaNumber(context.general.meta, `dex${siegeType}`);
|
||||
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||
|
||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
if (!isAttacker && statName === `dex${siegeType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 전투 특기: 공성
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_공성',
|
||||
name: '공성',
|
||||
info: '[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
|
||||
kind: 'war',
|
||||
selection: {
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_SIEGE],
|
||||
},
|
||||
getName: () => '공성',
|
||||
getInfo: () =>
|
||||
'[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
|
||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||
if (turnType === '징병' || turnType === '모병') {
|
||||
const armType = getAuxArmType(aux);
|
||||
if (varType === 'cost' && armType === 5) {
|
||||
return value * 0.9;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getWarPowerMultiplier: (_context, _unit, oppose) => {
|
||||
if (oppose instanceof WarUnitCity) {
|
||||
return [2, 1];
|
||||
}
|
||||
return [1, 1];
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
import { getAuxArmType, parseWarDexAux } from './aux.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warAvoidRatio') {
|
||||
return (value as number) + 0.2;
|
||||
}
|
||||
|
||||
if (!('unit' in context) || !context.unit) {
|
||||
return value;
|
||||
}
|
||||
const unit = context.unit;
|
||||
|
||||
const archerType = unit.getGameConfig().armTypes.archer;
|
||||
if (archerType === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (statName.startsWith('dex')) {
|
||||
const myDex = getMetaNumber(context.general.meta, `dex${archerType}`);
|
||||
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||
|
||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
if (!isAttacker && statName === `dex${archerType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 전투 특기: 궁병
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_궁병',
|
||||
name: '궁병',
|
||||
info: '[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
|
||||
kind: 'war',
|
||||
selection: {
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
requirements: [
|
||||
TraitRequirement.STAT_LEADERSHIP |
|
||||
TraitRequirement.REQ_DEXTERITY |
|
||||
TraitRequirement.ARMY_ARCHER |
|
||||
TraitRequirement.STAT_NOT_INTEL,
|
||||
TraitRequirement.STAT_STRENGTH | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_ARCHER,
|
||||
],
|
||||
},
|
||||
getName: () => '궁병',
|
||||
getInfo: () =>
|
||||
'[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
|
||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||
if (turnType === '징병' || turnType === '모병') {
|
||||
const armType = getAuxArmType(aux);
|
||||
// Note: In a real scenario, we should check if aux.armType is archer.
|
||||
// Since we don't have easy access to config here, we might need to assume legacy ID 2 or similar.
|
||||
if (varType === 'cost' && armType === 2) {
|
||||
return value * 0.9;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
import { getAuxArmType, parseWarDexAux } from './aux.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warMagicSuccessProb') {
|
||||
return (value as number) + 0.2;
|
||||
}
|
||||
|
||||
if (!('unit' in context) || !context.unit) {
|
||||
return value;
|
||||
}
|
||||
const unit = context.unit;
|
||||
|
||||
const wizardType = unit.getGameConfig().armTypes.wizard;
|
||||
if (wizardType === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (statName.startsWith('dex')) {
|
||||
const myDex = getMetaNumber(context.general.meta, `dex${wizardType}`);
|
||||
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||
|
||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
if (!isAttacker && statName === `dex${wizardType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 전투 특기: 귀병
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_귀병',
|
||||
name: '귀병',
|
||||
info: '[군사] 귀병 계통 징·모병비 -10%<br>[전투] 계략 성공 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 귀병 숙련을 가산',
|
||||
kind: 'war',
|
||||
selection: {
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
requirements: [
|
||||
TraitRequirement.STAT_INTEL |
|
||||
TraitRequirement.ARMY_WIZARD |
|
||||
TraitRequirement.REQ_DEXTERITY |
|
||||
TraitRequirement.STAT_NOT_STRENGTH,
|
||||
],
|
||||
},
|
||||
getName: () => '귀병',
|
||||
getInfo: () =>
|
||||
'[군사] 귀병 계통 징·모병비 -10%<br>[전투] 계략 성공 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 귀병 숙련을 가산',
|
||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||
if (turnType === '징병' || turnType === '모병') {
|
||||
const armType = getAuxArmType(aux);
|
||||
if (varType === 'cost' && armType === 4) {
|
||||
return value * 0.9;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
import { getAuxArmType, parseWarDexAux } from './aux.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (!('unit' in context) || !context.unit) {
|
||||
return value;
|
||||
}
|
||||
const unit = context.unit;
|
||||
|
||||
const cavalryType = unit.getGameConfig().armTypes.cavalry;
|
||||
if (cavalryType === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (statName.startsWith('dex')) {
|
||||
const myDex = getMetaNumber(context.general.meta, `dex${cavalryType}`);
|
||||
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||
|
||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
if (!isAttacker && statName === `dex${cavalryType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 전투 특기: 기병
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_기병',
|
||||
name: '기병',
|
||||
info: '[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
|
||||
kind: 'war',
|
||||
selection: {
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
requirements: [
|
||||
TraitRequirement.STAT_LEADERSHIP |
|
||||
TraitRequirement.REQ_DEXTERITY |
|
||||
TraitRequirement.ARMY_CAVALRY |
|
||||
TraitRequirement.STAT_NOT_INTEL,
|
||||
TraitRequirement.STAT_STRENGTH | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_CAVALRY,
|
||||
],
|
||||
},
|
||||
getName: () => '기병',
|
||||
getInfo: () =>
|
||||
'[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
|
||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||
if (turnType === '징병' || turnType === '모병') {
|
||||
const armType = getAuxArmType(aux);
|
||||
if (varType === 'cost' && armType === 3) {
|
||||
return value * 0.9;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getWarPowerMultiplier: (_context, unit, _oppose) => {
|
||||
if (unit.isAttacker()) {
|
||||
return [1.2, 1];
|
||||
}
|
||||
return [1.1, 1];
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_돌격지속 } from '@sammo-ts/logic/war/triggers/che_돌격.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'initWarPhase') {
|
||||
return (value as number) + 2;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_돌격',
|
||||
name: '돌격',
|
||||
info: '[전투] 공격 시 대등/유리한 병종에게는 퇴각 전까지 전투, 공격 시 페이즈 + 2, 공격 시 대미지 +5%',
|
||||
kind: 'war',
|
||||
selection: {
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
},
|
||||
getName: () => '돌격',
|
||||
getInfo: () => '[전투] 공격 시 대등/유리한 병종에게는 퇴각 전까지 전투, 공격 시 페이즈 + 2, 공격 시 대미지 +5%',
|
||||
getWarPowerMultiplier: (_context, unit, _oppose) => {
|
||||
if (unit.isAttacker()) {
|
||||
return [1.05, 1];
|
||||
}
|
||||
return [1, 1];
|
||||
},
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_돌격지속(_context.unit));
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
|
||||
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
|
||||
type WarUnitWithGeneral = WarUnit & { getGeneral: () => { meta: Record<string, unknown> } };
|
||||
|
||||
const hasGeneral = (unit: WarUnit): unit is WarUnitWithGeneral =>
|
||||
'getGeneral' in unit && typeof (unit as { getGeneral?: unknown }).getGeneral === 'function';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
const isAttacker = typeof aux === 'object' && aux !== null && (aux as { isAttacker?: unknown }).isAttacker === true;
|
||||
|
||||
if (statName === 'warCriticalRatio' && isAttacker && typeof value === 'number') {
|
||||
return value + 0.1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_무쌍',
|
||||
name: '무쌍',
|
||||
info: '[전투] 대미지 +5%, 피해 -2%, 공격 시 필살 확률 +10%p, <br>승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)<br>승리 수의 로그 비례로 피해 감소(10회 ⇒ -2%, 40회 ⇒ -6%)',
|
||||
kind: 'war',
|
||||
getName: () => '무쌍',
|
||||
getInfo: () =>
|
||||
'[전투] 대미지 +5%, 피해 -2%, 공격 시 필살 확률 +10%p, <br>승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)<br>승리 수의 로그 비례로 피해 감소(10회 ⇒ -2%, 40회 ⇒ -6%)',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getWarPowerMultiplier: (_context, unit, _oppose) => {
|
||||
let attackMultiplier = 1.05;
|
||||
let defenceMultiplier = 0.98;
|
||||
// Note: unit.getGeneral() is only available for WarUnitGeneral.
|
||||
// In a real scenario, we should check if unit is WarUnitGeneral.
|
||||
if (hasGeneral(unit)) {
|
||||
const killnum = getMetaNumber(unit.getGeneral().meta as Record<string, TriggerValue>, 'rank_killnum', 0);
|
||||
const logVal = Math.log2(Math.max(1, killnum / 5));
|
||||
attackMultiplier += logVal / 20;
|
||||
defenceMultiplier -= logVal / 50;
|
||||
}
|
||||
return [attackMultiplier, defenceMultiplier];
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_반계발동, che_반계시도 } from '@sammo-ts/logic/war/triggers/che_반계.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warMagicSuccessDamage' && aux === '반목') {
|
||||
return (value as number) + 0.9;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_반계',
|
||||
name: '반계',
|
||||
info: '[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)',
|
||||
kind: 'war',
|
||||
getName: () => '반계',
|
||||
getInfo: () =>
|
||||
'[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
onCalcOpposeStat: ((_context, statName, value, _aux) => {
|
||||
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
|
||||
return value - 0.1;
|
||||
}
|
||||
return value;
|
||||
}) as TraitModule['onCalcOpposeStat'],
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_반계시도(_context.unit), new che_반계발동(_context.unit));
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||
import { getAuxArmType, parseWarDexAux } from './aux.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (!('unit' in context) || !context.unit) {
|
||||
return value;
|
||||
}
|
||||
const unit = context.unit;
|
||||
|
||||
const footmanType = unit.getGameConfig().armTypes.footman;
|
||||
if (footmanType === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (statName.startsWith('dex')) {
|
||||
const myDex = getMetaNumber(context.general.meta, `dex${footmanType}`);
|
||||
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||
|
||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
if (!isAttacker && statName === `dex${footmanType}`) {
|
||||
return (value as number) + myDex;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 전투 특기: 보병
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_보병',
|
||||
name: '보병',
|
||||
info: '[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
|
||||
kind: 'war',
|
||||
selection: {
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
requirements: [
|
||||
TraitRequirement.STAT_LEADERSHIP |
|
||||
TraitRequirement.REQ_DEXTERITY |
|
||||
TraitRequirement.ARMY_FOOTMAN |
|
||||
TraitRequirement.STAT_NOT_INTEL,
|
||||
TraitRequirement.STAT_STRENGTH | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_FOOTMAN,
|
||||
],
|
||||
},
|
||||
getName: () => '보병',
|
||||
getInfo: () =>
|
||||
'[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
|
||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||
if (turnType === '징병' || turnType === '모병') {
|
||||
const armType = getAuxArmType(aux);
|
||||
// Note: In a real scenario, we should check if aux.armType is footman.
|
||||
// Since we don't have easy access to config here, we might need to assume legacy ID 1 or similar.
|
||||
if (varType === 'cost' && armType === 1) {
|
||||
return value * 0.9;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getWarPowerMultiplier: (_context, unit, _oppose) => {
|
||||
if (unit.isAttacker()) {
|
||||
return [1, 0.9];
|
||||
}
|
||||
return [1, 0.8];
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warMagicTrialProb') {
|
||||
return (value as number) + 0.2;
|
||||
}
|
||||
if (statName === 'warMagicSuccessProb') {
|
||||
return (value as number) + 0.2;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_신산',
|
||||
name: '신산',
|
||||
info: '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p<br>[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p',
|
||||
kind: 'war',
|
||||
getName: () => '신산',
|
||||
getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p<br>[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
onCalcDomestic: (_context, turnType, varType, value, _aux) => {
|
||||
if (turnType === '계략') {
|
||||
if (varType === 'success') return value + 0.1;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warMagicSuccessProb') {
|
||||
return (value as number) + 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_신중',
|
||||
name: '신중',
|
||||
info: '[전투] 계략 성공 확률 100%',
|
||||
kind: 'war',
|
||||
getName: () => '신중',
|
||||
getInfo: () => '[전투] 계략 성공 확률 100%',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_위압발동, che_위압시도 } from '@sammo-ts/logic/war/triggers/che_위압.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
_statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_위압',
|
||||
name: '위압',
|
||||
info: '[전투] 첫 페이즈 위압 발동(적 공격, 회피 불가, 사기 5 감소)',
|
||||
kind: 'war',
|
||||
getName: () => '위압',
|
||||
getInfo: () => '[전투] 첫 페이즈 위압 발동(적 공격, 회피 불가, 사기 5 감소)',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_STRENGTH],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_위압시도(_context.unit), new che_위압발동(_context.unit));
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
|
||||
import { triggerModule as cheUisulTriggerModule } from '@sammo-ts/logic/war/triggers/che_의술.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
// 전투 특기: 의술
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_의술',
|
||||
name: '의술',
|
||||
info: '[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)',
|
||||
kind: 'war',
|
||||
getName: () => '의술',
|
||||
getInfo: () =>
|
||||
'[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
|
||||
weight: 2,
|
||||
weightType: TraitWeightType.PERCENT,
|
||||
},
|
||||
getPreTurnExecuteTriggerList: (context) => new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general)),
|
||||
getBattlePhaseTriggerList: (context: WarActionContext) => {
|
||||
const unit = context.unit;
|
||||
if (!unit) {
|
||||
return null;
|
||||
}
|
||||
return cheUisulTriggerModule.createTriggerList(unit);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_저격발동, che_저격시도 } from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
_statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_저격',
|
||||
name: '저격',
|
||||
info: '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20',
|
||||
kind: 'war',
|
||||
getName: () => '저격',
|
||||
getInfo: () => '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(
|
||||
new che_저격시도(_context.unit, BaseWarUnitTrigger.TYPE_NONE, 0.5, 20, 40, 20),
|
||||
new che_저격발동(_context.unit, BaseWarUnitTrigger.TYPE_NONE)
|
||||
);
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warMagicSuccessDamage') {
|
||||
return (value as number) * 1.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_집중',
|
||||
name: '집중',
|
||||
info: '[전투] 계략 성공 시 대미지 +50%',
|
||||
kind: 'war',
|
||||
getName: () => '집중',
|
||||
getInfo: () => '[전투] 계략 성공 시 대미지 +50%',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
const RECRUIT_TRAIN = 70;
|
||||
const CONSCRIPT_TRAIN = 84;
|
||||
|
||||
const resolveLeadershipBonus = (
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
const base = context.general.stats.leadership;
|
||||
return value + base * 0.25;
|
||||
};
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName !== 'leadership') {
|
||||
return value;
|
||||
}
|
||||
return resolveLeadershipBonus(context, value);
|
||||
}
|
||||
|
||||
// 전투 특기: 징병
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_징병',
|
||||
name: '징병',
|
||||
info: '[군사] 징병/모병 시 훈사 70/84 제공<br>[기타] 통솔 순수 능력치 보정 +25%, 징병/모병/소집해제 시 인구 변동 없음',
|
||||
kind: 'war',
|
||||
getName: () => '징병',
|
||||
getInfo: () =>
|
||||
'[군사] 징병/모병 시 훈사 70/84 제공<br>[기타] 통솔 순수 능력치 보정 +25%, 징병/모병/소집해제 시 인구 변동 없음',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
onCalcDomestic: (_context, turnType, varType, value) => {
|
||||
if (turnType === '징병' || turnType === '모병') {
|
||||
if (varType === 'train' || varType === 'atmos') {
|
||||
return turnType === '징병' ? RECRUIT_TRAIN : CONSCRIPT_TRAIN;
|
||||
}
|
||||
}
|
||||
if (turnType === '징집인구' && varType === 'score') {
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
_statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_척사',
|
||||
name: '척사',
|
||||
info: '[전투] 지역·도시 병종 상대로 대미지 +20%, 아군 피해 -20%',
|
||||
kind: 'war',
|
||||
getName: () => '척사',
|
||||
getInfo: () => '[전투] 지역·도시 병종 상대로 대미지 +20%, 아군 피해 -20%',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getWarPowerMultiplier: (_context, _unit, oppose) => {
|
||||
const opposeCrewType = oppose.getCrewType();
|
||||
if (opposeCrewType.reqCities() || opposeCrewType.reqRegions()) {
|
||||
return [1.2, 0.8];
|
||||
}
|
||||
return [1, 1];
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { che_필살강화_회피불가 } from '@sammo-ts/logic/war/triggers/che_필살.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warCriticalRatio') {
|
||||
return (value as number) + 0.3;
|
||||
}
|
||||
if (statName === 'criticalDamageRange') {
|
||||
const [rangeMin, rangeMax] = value as [number, number];
|
||||
return [(rangeMin + rangeMax) / 2, rangeMax];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_필살',
|
||||
name: '필살',
|
||||
info: '[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상',
|
||||
kind: 'war',
|
||||
getName: () => '필살',
|
||||
getInfo: () => '[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
|
||||
weight: 1,
|
||||
weightType: TraitWeightType.NORM,
|
||||
},
|
||||
getBattlePhaseTriggerList: (_context) => {
|
||||
if (!_context.unit) return null;
|
||||
return new WarTriggerCaller(new che_필살강화_회피불가(_context.unit));
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
import { TraitRequirement, TraitWeightType } from '../requirements.js';
|
||||
|
||||
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||
function onCalcStat(
|
||||
context: WarActionContext,
|
||||
statName: WarStatName,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: GeneralStatName | WarStatName,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number] {
|
||||
if (statName === 'warMagicSuccessProb') {
|
||||
return (value as number) + 0.1;
|
||||
}
|
||||
if (statName === 'warMagicSuccessDamage') {
|
||||
return (value as number) * 1.3;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const traitModule: TraitModule = {
|
||||
key: 'che_환술',
|
||||
name: '환술',
|
||||
info: '[전투] 계략 성공 확률 +10%p, 계략 성공 시 대미지 +30%',
|
||||
kind: 'war',
|
||||
getName: () => '환술',
|
||||
getInfo: () => '[전투] 계략 성공 확률 +10%p, 계략 성공 시 대미지 +30%',
|
||||
selection: {
|
||||
requirements: [TraitRequirement.STAT_INTEL],
|
||||
weight: 5,
|
||||
weightType: TraitWeightType.PERCENT,
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
|
||||
|
||||
export const WAR_TRAIT_KEYS = [
|
||||
'che_귀병',
|
||||
'che_신산',
|
||||
'che_환술',
|
||||
'che_집중',
|
||||
'che_신중',
|
||||
'che_반계',
|
||||
'che_보병',
|
||||
'che_궁병',
|
||||
'che_기병',
|
||||
'che_공성',
|
||||
'che_돌격',
|
||||
'che_무쌍',
|
||||
'che_견고',
|
||||
'che_위압',
|
||||
'che_저격',
|
||||
'che_필살',
|
||||
'che_징병',
|
||||
'che_의술',
|
||||
'che_격노',
|
||||
'che_척사',
|
||||
] as const;
|
||||
|
||||
export type WarTraitKey = (typeof WAR_TRAIT_KEYS)[number];
|
||||
|
||||
export type WarTraitModule = TraitModule;
|
||||
|
||||
export type WarTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
const defaultImporters: Record<WarTraitKey, WarTraitImporter> = {
|
||||
che_귀병: async () => import('./che_귀병.js'),
|
||||
che_신산: async () => import('./che_신산.js'),
|
||||
che_환술: async () => import('./che_환술.js'),
|
||||
che_집중: async () => import('./che_집중.js'),
|
||||
che_신중: async () => import('./che_신중.js'),
|
||||
che_반계: async () => import('./che_반계.js'),
|
||||
che_보병: async () => import('./che_보병.js'),
|
||||
che_궁병: async () => import('./che_궁병.js'),
|
||||
che_기병: async () => import('./che_기병.js'),
|
||||
che_공성: async () => import('./che_공성.js'),
|
||||
che_돌격: async () => import('./che_돌격.js'),
|
||||
che_무쌍: async () => import('./che_무쌍.js'),
|
||||
che_견고: async () => import('./che_견고.js'),
|
||||
che_위압: async () => import('./che_위압.js'),
|
||||
che_저격: async () => import('./che_저격.js'),
|
||||
che_필살: async () => import('./che_필살.js'),
|
||||
che_징병: async () => import('./che_징병.js'),
|
||||
che_의술: async () => import('./che_의술.js'),
|
||||
che_격노: async () => import('./che_격노.js'),
|
||||
che_척사: async () => import('./che_척사.js'),
|
||||
};
|
||||
|
||||
export const isWarTraitKey = (value: string): value is WarTraitKey => WAR_TRAIT_KEYS.includes(value as WarTraitKey);
|
||||
|
||||
export class WarTraitLoader {
|
||||
private readonly cache = new Map<WarTraitKey, Promise<WarTraitModule>>();
|
||||
|
||||
constructor(private readonly importers: Record<WarTraitKey, WarTraitImporter> = defaultImporters) {}
|
||||
|
||||
async load(key: WarTraitKey): Promise<WarTraitModule> {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const importer = this.importers[key];
|
||||
if (!importer) {
|
||||
throw new Error(`Unknown war trait key: ${key}`);
|
||||
}
|
||||
const loading = importer().then((module) => {
|
||||
if (!('traitModule' in module)) {
|
||||
throw new Error(`Missing traitModule for war trait: ${key}`);
|
||||
}
|
||||
const resolved = module.traitModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(`War trait key mismatch: expected ${key}, got ${resolved.key}`);
|
||||
}
|
||||
if (resolved.kind !== 'war') {
|
||||
throw new Error(`War trait kind mismatch: ${resolved.key}`);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
this.cache.set(key, loading);
|
||||
return loading;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadWarTraitModules = async (
|
||||
keys: WarTraitKey[],
|
||||
loader: WarTraitLoader = new WarTraitLoader()
|
||||
): Promise<WarTraitModule[]> => {
|
||||
const modules: WarTraitModule[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const key of keys) {
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
modules.push(await loader.load(key));
|
||||
}
|
||||
return modules;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
export type TriggerDomesticActionType =
|
||||
| '상업'
|
||||
| '농업'
|
||||
| '성벽'
|
||||
| '수비'
|
||||
| '치안'
|
||||
| '인재탐색'
|
||||
| '징병'
|
||||
| '징집인구'
|
||||
| '계략'
|
||||
| '민심'
|
||||
| '인구'
|
||||
| '기술'
|
||||
| '모병'
|
||||
| '단련'
|
||||
| '조달';
|
||||
|
||||
export type TriggerDomesticVarType = 'cost' | 'score' | 'success' | 'fail' | 'train' | 'atmos' | 'rice' | 'probability';
|
||||
|
||||
export type TriggerStrategicActionType =
|
||||
'의병모집' | '허보' | '필사즉생' | '백성동원' | '이호경식' | '수몰' | '급습' | '피장파장';
|
||||
|
||||
export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
||||
|
||||
export type TriggerNationalIncomeType = 'gold' | 'rice' | 'pop';
|
||||
|
||||
export type GeneralStatName =
|
||||
| 'leadership'
|
||||
| 'strength'
|
||||
| 'intelligence'
|
||||
| 'experience'
|
||||
| 'dedication'
|
||||
| 'sabotageDefence'
|
||||
| 'sabotageAttack'
|
||||
| 'injuryProb'
|
||||
| 'addDex';
|
||||
|
||||
export type WarStatName =
|
||||
| GeneralStatName
|
||||
| 'cityBattleOrder'
|
||||
| 'initWarPhase'
|
||||
| 'bonusTrain'
|
||||
| 'bonusAtmos'
|
||||
| 'warCriticalRatio'
|
||||
| 'warAvoidRatio'
|
||||
| 'injuryProb'
|
||||
| 'killRice'
|
||||
| 'criticalDamageRange'
|
||||
| 'warMagicSuccessDamage'
|
||||
| 'warMagicFailDamage'
|
||||
| 'warMagicTrialProb'
|
||||
| 'warMagicSuccessProb'
|
||||
| 'addDex'
|
||||
| `dex${number}`;
|
||||
Reference in New Issue
Block a user