diff --git a/packages/logic/src/triggers/general.ts b/packages/logic/src/triggers/general.ts index 9019d08..1c2bddc 100644 --- a/packages/logic/src/triggers/general.ts +++ b/packages/logic/src/triggers/general.ts @@ -1,4 +1,4 @@ -import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; +import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js'; import type { RandomGenerator } from '@sammo-ts/common'; import type { WorldStateRepository } from '@sammo-ts/logic/ports/world.js'; import { TriggerCaller, type Trigger } from './core.js'; @@ -30,6 +30,7 @@ export const createGeneralSkillActivation = { general: General; + nation?: Nation | null; world?: WorldStateRepository; worldView?: GeneralWorldView; log?: GeneralActionLogSink; diff --git a/packages/logic/src/triggers/special/index.ts b/packages/logic/src/triggers/special/index.ts index 1efcc06..0afdd10 100644 --- a/packages/logic/src/triggers/special/index.ts +++ b/packages/logic/src/triggers/special/index.ts @@ -3,3 +3,4 @@ export * from './registry.js'; export * from './domestic/index.js'; export * from './war/index.js'; export * from './personality/index.js'; +export * from './nation/index.js'; diff --git a/packages/logic/src/triggers/special/nation/che_덕가.ts b/packages/logic/src/triggers/special/nation/che_덕가.ts new file mode 100644 index 0000000..fa07023 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_덕가.ts @@ -0,0 +1,26 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_도가.ts b/packages/logic/src/triggers/special/nation/che_도가.ts new file mode 100644 index 0000000..97f5ba6 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_도가.ts @@ -0,0 +1,22 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_도적.ts b/packages/logic/src/triggers/special/nation/che_도적.ts new file mode 100644 index 0000000..422cd5f --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_도적.ts @@ -0,0 +1,24 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_명가.ts b/packages/logic/src/triggers/special/nation/che_명가.ts new file mode 100644 index 0000000..1518b19 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_명가.ts @@ -0,0 +1,26 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_묵가.ts b/packages/logic/src/triggers/special/nation/che_묵가.ts new file mode 100644 index 0000000..2e9f151 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_묵가.ts @@ -0,0 +1,21 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_법가.ts b/packages/logic/src/triggers/special/nation/che_법가.ts new file mode 100644 index 0000000..4cab2de --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_법가.ts @@ -0,0 +1,26 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_병가.ts b/packages/logic/src/triggers/special/nation/che_병가.ts new file mode 100644 index 0000000..528a086 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_병가.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_불가.ts b/packages/logic/src/triggers/special/nation/che_불가.ts new file mode 100644 index 0000000..7ae897f --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_불가.ts @@ -0,0 +1,22 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_오두미도.ts b/packages/logic/src/triggers/special/nation/che_오두미도.ts new file mode 100644 index 0000000..d42befe --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_오두미도.ts @@ -0,0 +1,29 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_유가.ts b/packages/logic/src/triggers/special/nation/che_유가.ts new file mode 100644 index 0000000..29899fb --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_유가.ts @@ -0,0 +1,22 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_음양가.ts b/packages/logic/src/triggers/special/nation/che_음양가.ts new file mode 100644 index 0000000..02d41c6 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_음양가.ts @@ -0,0 +1,31 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_종횡가.ts b/packages/logic/src/triggers/special/nation/che_종횡가.ts new file mode 100644 index 0000000..b66a9db --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_종횡가.ts @@ -0,0 +1,34 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/che_중립.ts b/packages/logic/src/triggers/special/nation/che_중립.ts new file mode 100644 index 0000000..5721581 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_중립.ts @@ -0,0 +1,11 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 국가 성향: 중립 +export const traitModule: TraitModule = { + key: 'che_중립', + name: '중립', + info: '장점: 없음 / 단점: 없음', + kind: 'nation', + getName: () => '중립', + getInfo: () => '장점: 없음 / 단점: 없음', +}; diff --git a/packages/logic/src/triggers/special/nation/che_태평도.ts b/packages/logic/src/triggers/special/nation/che_태평도.ts new file mode 100644 index 0000000..4c702af --- /dev/null +++ b/packages/logic/src/triggers/special/nation/che_태평도.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/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; + }, +}; diff --git a/packages/logic/src/triggers/special/nation/index.ts b/packages/logic/src/triggers/special/nation/index.ts new file mode 100644 index 0000000..d8c7363 --- /dev/null +++ b/packages/logic/src/triggers/special/nation/index.ts @@ -0,0 +1,92 @@ +import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/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; + +const defaultImporters: Record = { + 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>(); + + constructor(private readonly importers: Record = defaultImporters) {} + + async load(key: NationTraitKey): Promise { + 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 => { + const modules: NationTraitModule[] = []; + 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 44a84d4..49924f1 100644 --- a/packages/logic/src/triggers/special/registry.ts +++ b/packages/logic/src/triggers/special/registry.ts @@ -26,6 +26,7 @@ const resolveTraitKey = ( specialWar: string | null; }; }; + nation?: { typeCode: string } | null; }, kind: TraitKind ): string | null => { @@ -35,6 +36,9 @@ const resolveTraitKey = ( if (kind === 'war') { return context.general.role.specialWar; } + if (kind === 'nation') { + return context.nation?.typeCode ?? null; + } return context.general.role.personality; }; @@ -51,6 +55,8 @@ const resolveModule = ( bucket = registry.domestic; } else if (kind === 'war') { bucket = registry.war; + } else if (kind === 'nation') { + bucket = registry.nation; } else { bucket = registry.personality; } @@ -201,10 +207,12 @@ export const createTraitModuleRegistry = []; war?: TraitModule[]; personality?: TraitModule[]; + nation?: TraitModule[]; }): TraitModuleRegistry => { const domestic = new Map>(); const war = new Map>(); const personality = new Map>(); + const nation = new Map>(); for (const module of options.domestic ?? []) { domestic.set(module.key, module); @@ -215,8 +223,11 @@ export const createTraitModuleRegistry = ('domestic', registry), new TraitGeneralActionRouter('war', registry), new TraitGeneralActionRouter('personality', registry), + new TraitGeneralActionRouter('nation', registry), ], war: [ new TraitWarActionRouter('domestic', registry), new TraitWarActionRouter('war', registry), new TraitWarActionRouter('personality', registry), + new TraitWarActionRouter('nation', registry), ], }); diff --git a/packages/logic/src/triggers/special/types.ts b/packages/logic/src/triggers/special/types.ts index c88fccf..1774c52 100644 --- a/packages/logic/src/triggers/special/types.ts +++ b/packages/logic/src/triggers/special/types.ts @@ -2,7 +2,7 @@ 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 TraitKind = 'domestic' | 'war' | 'personality'; +export type TraitKind = 'domestic' | 'war' | 'personality' | 'nation'; export interface TraitSpec { key: string; @@ -23,4 +23,5 @@ export interface TraitModuleRegistry>; war: Map>; personality: Map>; + nation: Map>; } diff --git a/packages/logic/src/triggers/types.ts b/packages/logic/src/triggers/types.ts index c355eaf..c422608 100644 --- a/packages/logic/src/triggers/types.ts +++ b/packages/logic/src/triggers/types.ts @@ -24,7 +24,7 @@ export type TriggerStrategicActionType = '의병모집' | '허보' | '필사즉 export type TriggerStrategicVarType = 'delay' | 'globalDelay'; -export type TriggerNationalIncomeType = 'gold' | 'rice'; +export type TriggerNationalIncomeType = 'gold' | 'rice' | 'pop'; export type GeneralStatName = 'leadership' | 'strength' | 'intelligence' | 'experience' | 'dedication';