diff --git a/packages/logic/src/triggers/special/domestic/che_발명.ts b/packages/logic/src/triggers/special/domestic/che_발명.ts index c2f02d8..83621c6 100644 --- a/packages/logic/src/triggers/special/domestic/che_발명.ts +++ b/packages/logic/src/triggers/special/domestic/che_발명.ts @@ -1,7 +1,7 @@ -import type { SpecialActionModule } from '@sammo-ts/logic/triggers/special/types.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; // 내정 특기: 발명 -export const specialModule: SpecialActionModule = { +export const traitModule: TraitModule = { key: 'che_발명', name: '발명', info: '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', diff --git a/packages/logic/src/triggers/special/domestic/che_인덕.ts b/packages/logic/src/triggers/special/domestic/che_인덕.ts index 40db71b..403ba22 100644 --- a/packages/logic/src/triggers/special/domestic/che_인덕.ts +++ b/packages/logic/src/triggers/special/domestic/che_인덕.ts @@ -1,7 +1,7 @@ -import type { SpecialActionModule } from '@sammo-ts/logic/triggers/special/types.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; // 내정 특기: 인덕 -export const specialModule: SpecialActionModule = { +export const traitModule: TraitModule = { key: 'che_인덕', name: '인덕', info: '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', diff --git a/packages/logic/src/triggers/special/domestic/index.ts b/packages/logic/src/triggers/special/domestic/index.ts index af11c51..91fa786 100644 --- a/packages/logic/src/triggers/special/domestic/index.ts +++ b/packages/logic/src/triggers/special/domestic/index.ts @@ -1,68 +1,68 @@ import type { - SpecialActionModule, - SpecialActionModuleExport, + TraitModule, + TraitModuleExport, } from '@sammo-ts/logic/triggers/special/types.js'; -export const DOMESTIC_SPECIAL_KEYS = [ +export const DOMESTIC_TRAIT_KEYS = [ 'che_인덕', 'che_발명', ] as const; -export type DomesticSpecialKey = - (typeof DOMESTIC_SPECIAL_KEYS)[number]; +export type DomesticTraitKey = + (typeof DOMESTIC_TRAIT_KEYS)[number]; -export type DomesticSpecialModule = SpecialActionModule; +export type DomesticTraitModule = TraitModule; -export type DomesticSpecialImporter = () => Promise; +export type DomesticTraitImporter = () => Promise; const defaultImporters: Record< - DomesticSpecialKey, - DomesticSpecialImporter + DomesticTraitKey, + DomesticTraitImporter > = { che_인덕: async () => import('./che_인덕.js'), che_발명: async () => import('./che_발명.js'), }; -export const isDomesticSpecialKey = ( +export const isDomesticTraitKey = ( value: string -): value is DomesticSpecialKey => - DOMESTIC_SPECIAL_KEYS.includes(value as DomesticSpecialKey); +): value is DomesticTraitKey => + DOMESTIC_TRAIT_KEYS.includes(value as DomesticTraitKey); -export class DomesticSpecialLoader { +export class DomesticTraitLoader { private readonly cache = new Map< - DomesticSpecialKey, - Promise + DomesticTraitKey, + Promise >(); constructor( private readonly importers: Record< - DomesticSpecialKey, - DomesticSpecialImporter + DomesticTraitKey, + DomesticTraitImporter > = defaultImporters ) {} - async load(key: DomesticSpecialKey): Promise { + async load(key: DomesticTraitKey): Promise { const cached = this.cache.get(key); if (cached) { return cached; } const importer = this.importers[key]; if (!importer) { - throw new Error(`Unknown domestic special key: ${key}`); + throw new Error(`Unknown domestic trait key: ${key}`); } const loading = importer().then((module) => { - if (!('specialModule' in module)) { - throw new Error(`Missing specialModule for domestic special: ${key}`); + if (!('traitModule' in module)) { + throw new Error(`Missing traitModule for domestic trait: ${key}`); } - const resolved = module.specialModule; + const resolved = module.traitModule; if (resolved.key !== key) { throw new Error( - `Domestic special key mismatch: expected ${key}, got ${resolved.key}` + `Domestic trait key mismatch: expected ${key}, got ${resolved.key}` ); } if (resolved.kind !== 'domestic') { throw new Error( - `Domestic special kind mismatch: ${resolved.key}` + `Domestic trait kind mismatch: ${resolved.key}` ); } return resolved; @@ -72,11 +72,11 @@ export class DomesticSpecialLoader { } } -export const loadDomesticSpecialModules = async ( - keys: DomesticSpecialKey[], - loader: DomesticSpecialLoader = new DomesticSpecialLoader() -): Promise => { - const modules: DomesticSpecialModule[] = []; +export const loadDomesticTraitModules = async ( + keys: DomesticTraitKey[], + loader: DomesticTraitLoader = new DomesticTraitLoader() +): Promise => { + const modules: DomesticTraitModule[] = []; const seen = new Set(); for (const key of keys) { if (seen.has(key)) { diff --git a/packages/logic/src/triggers/special/index.ts b/packages/logic/src/triggers/special/index.ts index aafab9d..1efcc06 100644 --- a/packages/logic/src/triggers/special/index.ts +++ b/packages/logic/src/triggers/special/index.ts @@ -2,3 +2,4 @@ export * from './types.js'; export * from './registry.js'; export * from './domestic/index.js'; export * from './war/index.js'; +export * from './personality/index.js'; diff --git a/packages/logic/src/triggers/special/personality/che_대의.ts b/packages/logic/src/triggers/special/personality/che_대의.ts new file mode 100644 index 0000000..d14b549 --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_대의.ts @@ -0,0 +1,43 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_안전.ts b/packages/logic/src/triggers/special/personality/che_안전.ts new file mode 100644 index 0000000..ea42e82 --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_안전.ts @@ -0,0 +1,44 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_왕좌.ts b/packages/logic/src/triggers/special/personality/che_왕좌.ts new file mode 100644 index 0000000..f4216e9 --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_왕좌.ts @@ -0,0 +1,43 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_유지.ts b/packages/logic/src/triggers/special/personality/che_유지.ts new file mode 100644 index 0000000..7ad46d2 --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_유지.ts @@ -0,0 +1,44 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_은둔.ts b/packages/logic/src/triggers/special/personality/che_은둔.ts new file mode 100644 index 0000000..475567b --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_은둔.ts @@ -0,0 +1,55 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_의협.ts b/packages/logic/src/triggers/special/personality/che_의협.ts new file mode 100644 index 0000000..ad3f31e --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_의협.ts @@ -0,0 +1,44 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_재간.ts b/packages/logic/src/triggers/special/personality/che_재간.ts new file mode 100644 index 0000000..c7210fe --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_재간.ts @@ -0,0 +1,44 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_정복.ts b/packages/logic/src/triggers/special/personality/che_정복.ts new file mode 100644 index 0000000..7e14467 --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_정복.ts @@ -0,0 +1,43 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_출세.ts b/packages/logic/src/triggers/special/personality/che_출세.ts new file mode 100644 index 0000000..57acbea --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_출세.ts @@ -0,0 +1,44 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_패권.ts b/packages/logic/src/triggers/special/personality/che_패권.ts new file mode 100644 index 0000000..b7ee010 --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_패권.ts @@ -0,0 +1,44 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/che_할거.ts b/packages/logic/src/triggers/special/personality/che_할거.ts new file mode 100644 index 0000000..9a9ae5a --- /dev/null +++ b/packages/logic/src/triggers/special/personality/che_할거.ts @@ -0,0 +1,43 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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/triggers/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, +}; diff --git a/packages/logic/src/triggers/special/personality/index.ts b/packages/logic/src/triggers/special/personality/index.ts new file mode 100644 index 0000000..b92de4d --- /dev/null +++ b/packages/logic/src/triggers/special/personality/index.ts @@ -0,0 +1,107 @@ +import type { + TraitModule, + TraitModuleExport, +} from '@sammo-ts/logic/triggers/special/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; + +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 + >(); + + constructor( + private readonly importers: Record< + PersonalityTraitKey, + PersonalityTraitImporter + > = defaultImporters + ) {} + + async load(key: PersonalityTraitKey): Promise { + 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 => { + const modules: PersonalityTraitModule[] = []; + const seen = new Set(); + for (const key of keys) { + if (seen.has(key)) { + continue; + } + seen.add(key); + modules.push(await loader.load(key)); + } + return modules; +}; diff --git a/packages/logic/src/triggers/special/registry.ts b/packages/logic/src/triggers/special/registry.ts index e3d1595..fb8f594 100644 --- a/packages/logic/src/triggers/special/registry.ts +++ b/packages/logic/src/triggers/special/registry.ts @@ -16,46 +16,64 @@ import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/acti import type { WarUnit } from '@sammo-ts/logic/war/units.js'; import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; import type { - SpecialActionKind, - SpecialActionModule, - SpecialActionModuleRegistry, + TraitKind, + TraitModule, + TraitModuleRegistry, } from './types.js'; -const resolveSpecialKey = ( - context: { general: { role: { specialDomestic: string | null; specialWar: string | null } } }, - kind: SpecialActionKind -): string | null => - kind === 'domestic' - ? context.general.role.specialDomestic - : context.general.role.specialWar; +const resolveTraitKey = ( + context: { + general: { + role: { + personality: string | null; + specialDomestic: string | null; + specialWar: string | null; + }; + }; + }, + kind: TraitKind +): string | null => { + if (kind === 'domestic') { + return context.general.role.specialDomestic; + } + if (kind === 'war') { + return context.general.role.specialWar; + } + return context.general.role.personality; +}; -const resolveModule = < - TriggerState extends GeneralTriggerState ->( - registry: SpecialActionModuleRegistry, - kind: SpecialActionKind, +const resolveModule = ( + registry: TraitModuleRegistry, + kind: TraitKind, key: string | null -): SpecialActionModule | null => { +): TraitModule | null => { if (!key) { return null; } - const bucket = kind === 'domestic' ? registry.domestic : registry.war; + let bucket: Map>; + if (kind === 'domestic') { + bucket = registry.domestic; + } else if (kind === 'war') { + bucket = registry.war; + } else { + bucket = registry.personality; + } return bucket.get(key) ?? null; }; -// General 파이프라인에서 특기 모듈을 선택해 위임하는 라우터. -export class SpecialGeneralActionRouter< +// General 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터. +export class TraitGeneralActionRouter< TriggerState extends GeneralTriggerState = GeneralTriggerState > implements GeneralActionModule { constructor( - private readonly kind: SpecialActionKind, - private readonly registry: SpecialActionModuleRegistry + private readonly kind: TraitKind, + private readonly registry: TraitModuleRegistry ) {} private getModule( context: GeneralActionContext - ): SpecialActionModule | null { - const key = resolveSpecialKey(context, this.kind); + ): TraitModule | null { + const key = resolveTraitKey(context, this.kind); return resolveModule(this.registry, this.kind, key); } @@ -135,19 +153,19 @@ export class SpecialGeneralActionRouter< } } -// 전투 파이프라인에서 특기 모듈을 선택해 위임하는 라우터. -export class SpecialWarActionRouter< +// 전투 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터. +export class TraitWarActionRouter< TriggerState extends GeneralTriggerState = GeneralTriggerState > implements WarActionModule { constructor( - private readonly kind: SpecialActionKind, - private readonly registry: SpecialActionModuleRegistry + private readonly kind: TraitKind, + private readonly registry: TraitModuleRegistry ) {} private getModule( context: WarActionContext - ): SpecialActionModule | null { - const key = resolveSpecialKey(context, this.kind); + ): TraitModule | null { + const key = resolveTraitKey(context, this.kind); return resolveModule(this.registry, this.kind, key); } @@ -195,21 +213,23 @@ export class SpecialWarActionRouter< } } -export interface SpecialActionModuleSet< +export interface TraitModuleSet< TriggerState extends GeneralTriggerState = GeneralTriggerState > { general: GeneralActionModule[]; war: WarActionModule[]; } -export const createSpecialActionModuleRegistry = < +export const createTraitModuleRegistry = < TriggerState extends GeneralTriggerState = GeneralTriggerState >(options: { - domestic?: SpecialActionModule[]; - war?: SpecialActionModule[]; -}): SpecialActionModuleRegistry => { - const domestic = new Map>(); - const war = new Map>(); + domestic?: TraitModule[]; + war?: TraitModule[]; + personality?: TraitModule[]; +}): TraitModuleRegistry => { + const domestic = new Map>(); + const war = new Map>(); + const personality = new Map>(); for (const module of options.domestic ?? []) { domestic.set(module.key, module); @@ -217,22 +237,27 @@ export const createSpecialActionModuleRegistry = < for (const module of options.war ?? []) { war.set(module.key, module); } + for (const module of options.personality ?? []) { + personality.set(module.key, module); + } - return { domestic, war }; + return { domestic, war, personality }; }; -// 특기 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다. -export const createSpecialActionModules = < +// 특성 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다. +export const createTraitModules = < TriggerState extends GeneralTriggerState = GeneralTriggerState >( - registry: SpecialActionModuleRegistry -): SpecialActionModuleSet => ({ + registry: TraitModuleRegistry +): TraitModuleSet => ({ general: [ - new SpecialGeneralActionRouter('domestic', registry), - new SpecialGeneralActionRouter('war', registry), + new TraitGeneralActionRouter('domestic', registry), + new TraitGeneralActionRouter('war', registry), + new TraitGeneralActionRouter('personality', registry), ], war: [ - new SpecialWarActionRouter('domestic', registry), - new SpecialWarActionRouter('war', registry), + new TraitWarActionRouter('domestic', registry), + new TraitWarActionRouter('war', registry), + new TraitWarActionRouter('personality', registry), ], }); diff --git a/packages/logic/src/triggers/special/types.ts b/packages/logic/src/triggers/special/types.ts index 72bc8a2..109fe98 100644 --- a/packages/logic/src/triggers/special/types.ts +++ b/packages/logic/src/triggers/special/types.ts @@ -2,30 +2,31 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js'; import type { WarActionModule } from '@sammo-ts/logic/war/actions.js'; -export type SpecialActionKind = 'domestic' | 'war'; +export type TraitKind = 'domestic' | 'war' | 'personality'; -export interface SpecialActionSpec { +export interface TraitSpec { key: string; name: string; info: string; - kind: SpecialActionKind; + kind: TraitKind; } -export type SpecialActionModule< +export type TraitModule< TriggerState extends GeneralTriggerState = GeneralTriggerState -> = SpecialActionSpec & +> = TraitSpec & GeneralActionModule & WarActionModule; -export interface SpecialActionModuleExport< +export interface TraitModuleExport< TriggerState extends GeneralTriggerState = GeneralTriggerState > { - specialModule: SpecialActionModule; + traitModule: TraitModule; } -export interface SpecialActionModuleRegistry< +export interface TraitModuleRegistry< TriggerState extends GeneralTriggerState = GeneralTriggerState > { - domestic: Map>; - war: Map>; + domestic: Map>; + war: Map>; + personality: Map>; } diff --git a/packages/logic/src/triggers/special/war/che_의술.ts b/packages/logic/src/triggers/special/war/che_의술.ts index f01f88a..4aa4811 100644 --- a/packages/logic/src/triggers/special/war/che_의술.ts +++ b/packages/logic/src/triggers/special/war/che_의술.ts @@ -2,10 +2,10 @@ 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 { SpecialActionModule } from '@sammo-ts/logic/triggers/special/types.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; // 전투 특기: 의술 -export const specialModule: SpecialActionModule = { +export const traitModule: TraitModule = { key: 'che_의술', name: '의술', info: '[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복
[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)', diff --git a/packages/logic/src/triggers/special/war/che_징병.ts b/packages/logic/src/triggers/special/war/che_징병.ts index 954aee6..a4986ae 100644 --- a/packages/logic/src/triggers/special/war/che_징병.ts +++ b/packages/logic/src/triggers/special/war/che_징병.ts @@ -1,7 +1,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; -import type { SpecialActionModule } from '@sammo-ts/logic/triggers/special/types.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; const RECRUIT_TRAIN = 70; const CONSCRIPT_TRAIN = 84; @@ -41,7 +41,7 @@ function onCalcStat( } // 전투 특기: 징병 -export const specialModule: SpecialActionModule = { +export const traitModule: TraitModule = { key: 'che_징병', name: '징병', info: '[군사] 징병/모병 시 훈사 70/84 제공
[기타] 통솔 순수 능력치 보정 +25%, 징병/모병/소집해제 시 인구 변동 없음', diff --git a/packages/logic/src/triggers/special/war/index.ts b/packages/logic/src/triggers/special/war/index.ts index e79e9b2..4b13247 100644 --- a/packages/logic/src/triggers/special/war/index.ts +++ b/packages/logic/src/triggers/special/war/index.ts @@ -1,67 +1,67 @@ import type { - SpecialActionModule, - SpecialActionModuleExport, + TraitModule, + TraitModuleExport, } from '@sammo-ts/logic/triggers/special/types.js'; -export const WAR_SPECIAL_KEYS = [ +export const WAR_TRAIT_KEYS = [ 'che_의술', 'che_징병', ] as const; -export type WarSpecialKey = - (typeof WAR_SPECIAL_KEYS)[number]; +export type WarTraitKey = + (typeof WAR_TRAIT_KEYS)[number]; -export type WarSpecialModule = SpecialActionModule; +export type WarTraitModule = TraitModule; -export type WarSpecialImporter = () => Promise; +export type WarTraitImporter = () => Promise; const defaultImporters: Record< - WarSpecialKey, - WarSpecialImporter + WarTraitKey, + WarTraitImporter > = { che_의술: async () => import('./che_의술.js'), che_징병: async () => import('./che_징병.js'), }; -export const isWarSpecialKey = ( +export const isWarTraitKey = ( value: string -): value is WarSpecialKey => - WAR_SPECIAL_KEYS.includes(value as WarSpecialKey); +): value is WarTraitKey => + WAR_TRAIT_KEYS.includes(value as WarTraitKey); -export class WarSpecialLoader { +export class WarTraitLoader { private readonly cache = new Map< - WarSpecialKey, - Promise + WarTraitKey, + Promise >(); constructor( private readonly importers: Record< - WarSpecialKey, - WarSpecialImporter + WarTraitKey, + WarTraitImporter > = defaultImporters ) {} - async load(key: WarSpecialKey): Promise { + async load(key: WarTraitKey): Promise { const cached = this.cache.get(key); if (cached) { return cached; } const importer = this.importers[key]; if (!importer) { - throw new Error(`Unknown war special key: ${key}`); + throw new Error(`Unknown war trait key: ${key}`); } const loading = importer().then((module) => { - if (!('specialModule' in module)) { - throw new Error(`Missing specialModule for war special: ${key}`); + if (!('traitModule' in module)) { + throw new Error(`Missing traitModule for war trait: ${key}`); } - const resolved = module.specialModule; + const resolved = module.traitModule; if (resolved.key !== key) { throw new Error( - `War special key mismatch: expected ${key}, got ${resolved.key}` + `War trait key mismatch: expected ${key}, got ${resolved.key}` ); } if (resolved.kind !== 'war') { - throw new Error(`War special kind mismatch: ${resolved.key}`); + throw new Error(`War trait kind mismatch: ${resolved.key}`); } return resolved; }); @@ -70,11 +70,11 @@ export class WarSpecialLoader { } } -export const loadWarSpecialModules = async ( - keys: WarSpecialKey[], - loader: WarSpecialLoader = new WarSpecialLoader() -): Promise => { - const modules: WarSpecialModule[] = []; +export const loadWarTraitModules = async ( + keys: WarTraitKey[], + loader: WarTraitLoader = new WarTraitLoader() +): Promise => { + const modules: WarTraitModule[] = []; const seen = new Set(); for (const key of keys) { if (seen.has(key)) { diff --git a/packages/logic/src/triggers/types.ts b/packages/logic/src/triggers/types.ts index 914459e..4f67c97 100644 --- a/packages/logic/src/triggers/types.ts +++ b/packages/logic/src/triggers/types.ts @@ -11,7 +11,8 @@ export type TriggerDomesticActionType = | '민심' | '인구' | '기술' - | '모병'; + | '모병' + | '단련'; export type TriggerDomesticVarType = | 'cost' @@ -36,7 +37,12 @@ export type TriggerStrategicVarType = 'delay' | 'globalDelay'; export type TriggerNationalIncomeType = 'gold' | 'rice'; -export type GeneralStatName = 'leadership' | 'strength' | 'intelligence'; +export type GeneralStatName = + | 'leadership' + | 'strength' + | 'intelligence' + | 'experience' + | 'dedication'; export type WarStatName = | GeneralStatName diff --git a/packages/logic/test/specialActions.test.ts b/packages/logic/test/specialActions.test.ts index a7b90eb..56493fd 100644 --- a/packages/logic/test/specialActions.test.ts +++ b/packages/logic/test/specialActions.test.ts @@ -8,10 +8,10 @@ import type { UnitSetDefinition } from '../src/world/types.js'; import { GeneralActionPipeline } from '../src/triggers/general-action.js'; import { createGeneralTriggerContext } from '../src/triggers/general.js'; import { - createSpecialActionModuleRegistry, - createSpecialActionModules, - loadDomesticSpecialModules, - loadWarSpecialModules, + createTraitModuleRegistry, + createTraitModules, + loadDomesticTraitModules, + loadWarTraitModules, } from '../src/triggers/special/index.js'; import { ActionLogger } from '../src/logging/actionLogger.js'; import { WarActionPipeline } from '../src/war/actions.js'; @@ -161,13 +161,13 @@ const buildUnitSet = (): UnitSetDefinition => ({ ], }); -describe('special action modules', () => { - it('loads special modules by key', async () => { - const domestic = await loadDomesticSpecialModules([ +describe('trait modules', () => { + it('loads trait modules by key', async () => { + const domestic = await loadDomesticTraitModules([ 'che_인덕', 'che_발명', ]); - const war = await loadWarSpecialModules(['che_의술', 'che_징병']); + const war = await loadWarTraitModules(['che_의술', 'che_징병']); expect(domestic.map((module) => module.key)).toEqual([ 'che_인덕', @@ -180,15 +180,15 @@ describe('special action modules', () => { }); it('applies domestic and war modifiers in general pipeline', async () => { - const domestic = await loadDomesticSpecialModules([ + const domestic = await loadDomesticTraitModules([ 'che_인덕', 'che_발명', ]); - const war = await loadWarSpecialModules(['che_의술', 'che_징병']); - const registry = createSpecialActionModuleRegistry({ domestic, war }); - const specialModules = createSpecialActionModules(registry); + const war = await loadWarTraitModules(['che_의술', 'che_징병']); + const registry = createTraitModuleRegistry({ domestic, war }); + const traitModules = createTraitModules(registry); - const pipeline = new GeneralActionPipeline(specialModules.general); + const pipeline = new GeneralActionPipeline(traitModules.general); const general = buildGeneral({ role: { personality: null, @@ -214,14 +214,14 @@ describe('special action modules', () => { }); it('heals city generals with 의술 pre-turn trigger', async () => { - const domestic = await loadDomesticSpecialModules([ + const domestic = await loadDomesticTraitModules([ 'che_인덕', 'che_발명', ]); - const war = await loadWarSpecialModules(['che_의술', 'che_징병']); - const registry = createSpecialActionModuleRegistry({ domestic, war }); - const specialModules = createSpecialActionModules(registry); - const pipeline = new GeneralActionPipeline(specialModules.general); + const war = await loadWarTraitModules(['che_의술', 'che_징병']); + const registry = createTraitModuleRegistry({ domestic, war }); + const traitModules = createTraitModules(registry); + const pipeline = new GeneralActionPipeline(traitModules.general); const general = buildGeneral({ injury: 20, @@ -270,13 +270,13 @@ describe('special action modules', () => { }); it('activates 의술 battle trigger and reduces damage', async () => { - const domestic = await loadDomesticSpecialModules([ + const domestic = await loadDomesticTraitModules([ 'che_인덕', 'che_발명', ]); - const war = await loadWarSpecialModules(['che_의술', 'che_징병']); - const registry = createSpecialActionModuleRegistry({ domestic, war }); - const specialModules = createSpecialActionModules(registry); + const war = await loadWarTraitModules(['che_의술', 'che_징병']); + const registry = createTraitModuleRegistry({ domestic, war }); + const traitModules = createTraitModules(registry); const rng = new RandUtil(new ConstantRNG(0)); const config = buildConfig(); @@ -308,7 +308,7 @@ describe('special action modules', () => { true, crewType, new ActionLogger({ generalId: 1, nationId: 1 }), - new WarActionPipeline(specialModules.war) + new WarActionPipeline(traitModules.war) ); const defender = new WarUnitCity( rng, diff --git a/packages/logic/vitest.config.ts b/packages/logic/vitest.config.ts index b39b7b9..6f26c92 100644 --- a/packages/logic/vitest.config.ts +++ b/packages/logic/vitest.config.ts @@ -6,7 +6,7 @@ import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [ tsconfigPaths({ - projects: [path.resolve(__dirname, '../../tsconfig.base.json')], + projects: [path.resolve(__dirname, '../../tsconfig.paths.json')], }), ], test: {