feat: 국가 성향 모듈 추가 및 관련 타입 수정

This commit is contained in:
2026-01-06 12:50:55 +00:00
parent 19851c6e08
commit 55a9230a36
20 changed files with 456 additions and 4 deletions
+2 -1
View File
@@ -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 = <TriggerState extends GeneralTrigger
export interface GeneralActionContext<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: General<TriggerState>;
nation?: Nation | null;
world?: WorldStateRepository;
worldView?: GeneralWorldView<TriggerState>;
log?: GeneralActionLogSink;
@@ -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';
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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;
},
};
@@ -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: () => '장점: 없음 / 단점: 없음',
};
@@ -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;
},
};
@@ -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<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;
};
@@ -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 = <TriggerState extends GeneralTriggerState>(
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 = <TriggerState extends GeneralTriggerSta
domestic?: TraitModule<TriggerState>[];
war?: TraitModule<TriggerState>[];
personality?: TraitModule<TriggerState>[];
nation?: TraitModule<TriggerState>[];
}): TraitModuleRegistry<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>>();
for (const module of options.domestic ?? []) {
domestic.set(module.key, module);
@@ -215,8 +223,11 @@ export const createTraitModuleRegistry = <TriggerState extends GeneralTriggerSta
for (const module of options.personality ?? []) {
personality.set(module.key, module);
}
for (const module of options.nation ?? []) {
nation.set(module.key, module);
}
return { domestic, war, personality };
return { domestic, war, personality, nation };
};
// 특성 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다.
@@ -227,10 +238,12 @@ export const createTraitModules = <TriggerState extends GeneralTriggerState = Ge
new TraitGeneralActionRouter<TriggerState>('domestic', registry),
new TraitGeneralActionRouter<TriggerState>('war', registry),
new TraitGeneralActionRouter<TriggerState>('personality', registry),
new TraitGeneralActionRouter<TriggerState>('nation', registry),
],
war: [
new TraitWarActionRouter<TriggerState>('domestic', registry),
new TraitWarActionRouter<TriggerState>('war', registry),
new TraitWarActionRouter<TriggerState>('personality', registry),
new TraitWarActionRouter<TriggerState>('nation', registry),
],
});
+2 -1
View File
@@ -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<TriggerState extends GeneralTriggerState =
domestic: Map<string, TraitModule<TriggerState>>;
war: Map<string, TraitModule<TriggerState>>;
personality: Map<string, TraitModule<TriggerState>>;
nation: Map<string, TraitModule<TriggerState>>;
}
+1 -1
View File
@@ -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';