feat: apply traits in live turn runtime

This commit is contained in:
2026-07-25 08:14:25 +00:00
parent 790f4915ab
commit 88afcb5465
24 changed files with 475 additions and 81 deletions
@@ -0,0 +1,78 @@
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-action.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import { createOfficerLevelActionModules } from './officerLevel.js';
import {
createTraitModuleRegistry,
DOMESTIC_TRAIT_KEYS,
loadDomesticTraitModules,
loadNationTraitModules,
loadPersonalityTraitModules,
loadWarTraitModules,
NATION_TRAIT_KEYS,
PERSONALITY_TRAIT_KEYS,
TraitGeneralActionRouter,
TraitWarActionRouter,
WAR_TRAIT_KEYS,
} from './special/index.js';
export interface ActionModuleBundle<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: GeneralActionModule<TriggerState>[];
war: WarActionModule<TriggerState>[];
itemModules: ItemModule<TriggerState>[];
}
// 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 traitRegistry = createTraitModuleRegistry<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: [
new TraitGeneralActionRouter('nation', traitRegistry),
officer.general,
new TraitGeneralActionRouter('domestic', traitRegistry),
new TraitGeneralActionRouter('war', traitRegistry),
new TraitGeneralActionRouter('personality', traitRegistry),
...(crewTypeCatalog ? [crewTypeCatalog.generalActionModule] : []),
inherit.general as GeneralActionModule<TriggerState>,
...items.general,
],
war: [
new TraitWarActionRouter('nation', traitRegistry),
officer.war,
new TraitWarActionRouter('domestic', traitRegistry),
new TraitWarActionRouter('war', traitRegistry),
new TraitWarActionRouter('personality', traitRegistry),
...(crewTypeCatalog ? [crewTypeCatalog.warActionModule] : []),
inherit.war as WarActionModule<TriggerState>,
...items.war,
],
itemModules,
};
};
+2
View File
@@ -2,4 +2,6 @@ export * from './core.js';
export * from './general.js';
export * from './general-action.js';
export * from './types.js';
export * from './officerLevel.js';
export * from './actionModuleBundle.js';
export * from './special/index.js';
@@ -0,0 +1,76 @@
import { asRecord } from '@sammo-ts/common';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionModule } from './general-action.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>,
});
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_덕가',
name: '덕가',
info: '장점: 치안↑ 인구↑ 민심↑ / 단점: 쌀수입↓ 수성↓',
info: '치안↑ 인구↑ 민심↑ 쌀수입↓ 수성↓',
kind: 'nation',
getName: () => '덕가',
getInfo: () => '장점: 치안↑ 인구↑ 민심↑ / 단점: 쌀수입↓ 수성↓',
getInfo: () => '치안↑ 인구↑ 민심↑ 쌀수입↓ 수성↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '치안' || turnType === '민심' || turnType === '인구') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_도가',
name: '도가',
info: '장점: 인구↑ / 단점: 기술↓ 치안↓',
info: '인구↑ 기술↓ 치안↓',
kind: 'nation',
getName: () => '도가',
getInfo: () => '장점: 인구↑ / 단점: 기술↓ 치안↓',
getInfo: () => '인구↑ 기술↓ 치안↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '기술' || turnType === '치안') {
if (varType === 'score') return value * 0.9;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_도적',
name: '도적',
info: '장점: 계략↑ / 단점: 금수입↓ 치안↓ 민심↓',
info: '계략↑ 금수입↓ 치안↓ 민심↓',
kind: 'nation',
getName: () => '도적',
getInfo: () => '장점: 계략↑ / 단점: 금수입↓ 치안↓ 민심↓',
getInfo: () => '계략↑ 금수입↓ 치안↓ 민심↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '치안' || turnType === '민심' || turnType === '인구') {
if (varType === 'score') return value * 0.9;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_명가',
name: '명가',
info: '장점: 기술↑ 인구↑ / 단점: 쌀수입↓ 수성↓',
info: '기술↑ 인구↑ 쌀수입↓ 수성↓',
kind: 'nation',
getName: () => '명가',
getInfo: () => '장점: 기술↑ 인구↑ / 단점: 쌀수입↓ 수성↓',
getInfo: () => '기술↑ 인구↑ 쌀수입↓ 수성↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '기술') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_묵가',
name: '묵가',
info: '장점: 수성↑ / 단점: 기술↓',
info: '수성↑ 기술↓',
kind: 'nation',
getName: () => '묵가',
getInfo: () => '장점: 수성↑ / 단점: 기술↓',
getInfo: () => '수성↑ 기술↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '수비' || turnType === '성벽') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_법가',
name: '법가',
info: '장점: 금수입↑ 치안↑ / 단점: 인구↓ 민심↓',
info: '금수입↑ 치안↑ 인구↓ 민심↓',
kind: 'nation',
getName: () => '법가',
getInfo: () => '장점: 금수입↑ 치안↑ / 단점: 인구↓ 민심↓',
getInfo: () => '금수입↑ 치안↑ 인구↓ 민심↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '치안') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_병가',
name: '병가',
info: '장점: 기술↑ 수성↑ / 단점: 인구↓ 민심↓',
info: '기술↑ 수성↑ 인구↓ 민심↓',
kind: 'nation',
getName: () => '병가',
getInfo: () => '장점: 기술↑ 수성↑ / 단점: 인구↓ 민심↓',
getInfo: () => '기술↑ 수성↑ 인구↓ 민심↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '기술' || turnType === '수비' || turnType === '성벽') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_불가',
name: '불가',
info: '장점: 민심↑ 수성↑ / 단점: 금수입↓',
info: '민심↑ 수성↑ 금수입↓',
kind: 'nation',
getName: () => '불가',
getInfo: () => '장점: 민심↑ 수성↑ / 단점: 금수입↓',
getInfo: () => '민심↑ 수성↑ 금수입↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '민심' || turnType === '인구' || turnType === '수비' || turnType === '성벽') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_오두미도',
name: '오두미도',
info: '장점: 쌀수입↑ 인구↑ / 단점: 기술↓ 수성↓ 농상↓',
info: '쌀수입↑ 인구↑ 기술↓ 수성↓ 농상↓',
kind: 'nation',
getName: () => '오두미도',
getInfo: () => '장점: 쌀수입↑ 인구↑ / 단점: 기술↓ 수성↓ 농상↓',
getInfo: () => '쌀수입↑ 인구↑ 기술↓ 수성↓ 농상↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (
turnType === '기술' ||
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_유가',
name: '유가',
info: '장점: 농상↑ 민심↑ / 단점: 쌀수입↓',
info: '농상↑ 민심↑ 쌀수입↓',
kind: 'nation',
getName: () => '유가',
getInfo: () => '장점: 농상↑ 민심↑ / 단점: 쌀수입↓',
getInfo: () => '농상↑ 민심↑ 쌀수입↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '농업' || turnType === '상업' || turnType === '민심' || turnType === '인구') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_음양가',
name: '음양가',
info: '장점: 농상↑ 인구↑ / 단점: 기술↓ 전략↓',
info: '농상↑ 인구↑ 기술↓ 전략↓',
kind: 'nation',
getName: () => '음양가',
getInfo: () => '장점: 농상↑ 인구↑ / 단점: 기술↓ 전략↓',
getInfo: () => '농상↑ 인구↑ 기술↓ 전략↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '농업' || turnType === '상업') {
if (varType === 'score') return value * 1.1;
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_종횡가',
name: '종횡가',
info: '장점: 전략↑ 수성↑ / 단점: 금수입↓ 농상↓',
info: '전략↑ 수성↑ 금수입↓ 농상↓',
kind: 'nation',
getName: () => '종횡가',
getInfo: () => '장점: 전략↑ 수성↑ / 단점: 금수입↓ 농상↓',
getInfo: () => '전략↑ 수성↑ 금수입↓ 농상↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '수비' || turnType === '성벽') {
if (varType === 'score') return value * 1.1;
@@ -3,9 +3,9 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 국가 성향: 중립
export const traitModule: TraitModule = {
key: 'che_중립',
name: '중립',
info: '장점: 없음 / 단점: 없음',
name: '-',
info: ' ',
kind: 'nation',
getName: () => '중립',
getInfo: () => '장점: 없음 / 단점: 없음',
getName: () => '-',
getInfo: () => ' ',
};
@@ -4,10 +4,10 @@ import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
export const traitModule: TraitModule = {
key: 'che_태평도',
name: '태평도',
info: '장점: 인구↑ 민심↑ / 단점: 기술↓ 수성↓',
info: '인구↑ 민심↑ 기술↓ 수성↓',
kind: 'nation',
getName: () => '태평도',
getInfo: () => '장점: 인구↑ 민심↑ / 단점: 기술↓ 수성↓',
getInfo: () => '인구↑ 민심↑ 기술↓ 수성↓',
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '민심' || turnType === '인구') {
if (varType === 'score') return value * 1.1;