refactor: rename SpecialAction to Trait and update related types
- Changed SpecialActionKind to TraitKind and updated its usage across the codebase. - Refactored SpecialActionSpec to TraitSpec and updated all references. - Updated the SpecialActionModule and related interfaces to TraitModule. - Added personality traits with corresponding modules and loaders. - Updated tests to reflect the new Trait structure and naming conventions. - Adjusted imports and exports to align with the new Trait system.
This commit is contained in:
@@ -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_발명',
|
key: 'che_발명',
|
||||||
name: '발명',
|
name: '발명',
|
||||||
info: '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
info: '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||||
|
|||||||
@@ -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_인덕',
|
key: 'che_인덕',
|
||||||
name: '인덕',
|
name: '인덕',
|
||||||
info: '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
info: '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||||
|
|||||||
@@ -1,68 +1,68 @@
|
|||||||
import type {
|
import type {
|
||||||
SpecialActionModule,
|
TraitModule,
|
||||||
SpecialActionModuleExport,
|
TraitModuleExport,
|
||||||
} from '@sammo-ts/logic/triggers/special/types.js';
|
} from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
|
|
||||||
export const DOMESTIC_SPECIAL_KEYS = [
|
export const DOMESTIC_TRAIT_KEYS = [
|
||||||
'che_인덕',
|
'che_인덕',
|
||||||
'che_발명',
|
'che_발명',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type DomesticSpecialKey =
|
export type DomesticTraitKey =
|
||||||
(typeof DOMESTIC_SPECIAL_KEYS)[number];
|
(typeof DOMESTIC_TRAIT_KEYS)[number];
|
||||||
|
|
||||||
export type DomesticSpecialModule = SpecialActionModule;
|
export type DomesticTraitModule = TraitModule;
|
||||||
|
|
||||||
export type DomesticSpecialImporter = () => Promise<SpecialActionModuleExport>;
|
export type DomesticTraitImporter = () => Promise<TraitModuleExport>;
|
||||||
|
|
||||||
const defaultImporters: Record<
|
const defaultImporters: Record<
|
||||||
DomesticSpecialKey,
|
DomesticTraitKey,
|
||||||
DomesticSpecialImporter
|
DomesticTraitImporter
|
||||||
> = {
|
> = {
|
||||||
che_인덕: async () => import('./che_인덕.js'),
|
che_인덕: async () => import('./che_인덕.js'),
|
||||||
che_발명: async () => import('./che_발명.js'),
|
che_발명: async () => import('./che_발명.js'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isDomesticSpecialKey = (
|
export const isDomesticTraitKey = (
|
||||||
value: string
|
value: string
|
||||||
): value is DomesticSpecialKey =>
|
): value is DomesticTraitKey =>
|
||||||
DOMESTIC_SPECIAL_KEYS.includes(value as DomesticSpecialKey);
|
DOMESTIC_TRAIT_KEYS.includes(value as DomesticTraitKey);
|
||||||
|
|
||||||
export class DomesticSpecialLoader {
|
export class DomesticTraitLoader {
|
||||||
private readonly cache = new Map<
|
private readonly cache = new Map<
|
||||||
DomesticSpecialKey,
|
DomesticTraitKey,
|
||||||
Promise<DomesticSpecialModule>
|
Promise<DomesticTraitModule>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly importers: Record<
|
private readonly importers: Record<
|
||||||
DomesticSpecialKey,
|
DomesticTraitKey,
|
||||||
DomesticSpecialImporter
|
DomesticTraitImporter
|
||||||
> = defaultImporters
|
> = defaultImporters
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async load(key: DomesticSpecialKey): Promise<DomesticSpecialModule> {
|
async load(key: DomesticTraitKey): Promise<DomesticTraitModule> {
|
||||||
const cached = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
const importer = this.importers[key];
|
const importer = this.importers[key];
|
||||||
if (!importer) {
|
if (!importer) {
|
||||||
throw new Error(`Unknown domestic special key: ${key}`);
|
throw new Error(`Unknown domestic trait key: ${key}`);
|
||||||
}
|
}
|
||||||
const loading = importer().then((module) => {
|
const loading = importer().then((module) => {
|
||||||
if (!('specialModule' in module)) {
|
if (!('traitModule' in module)) {
|
||||||
throw new Error(`Missing specialModule for domestic special: ${key}`);
|
throw new Error(`Missing traitModule for domestic trait: ${key}`);
|
||||||
}
|
}
|
||||||
const resolved = module.specialModule;
|
const resolved = module.traitModule;
|
||||||
if (resolved.key !== key) {
|
if (resolved.key !== key) {
|
||||||
throw new Error(
|
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') {
|
if (resolved.kind !== 'domestic') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Domestic special kind mismatch: ${resolved.key}`
|
`Domestic trait kind mismatch: ${resolved.key}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
@@ -72,11 +72,11 @@ export class DomesticSpecialLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadDomesticSpecialModules = async (
|
export const loadDomesticTraitModules = async (
|
||||||
keys: DomesticSpecialKey[],
|
keys: DomesticTraitKey[],
|
||||||
loader: DomesticSpecialLoader = new DomesticSpecialLoader()
|
loader: DomesticTraitLoader = new DomesticTraitLoader()
|
||||||
): Promise<DomesticSpecialModule[]> => {
|
): Promise<DomesticTraitModule[]> => {
|
||||||
const modules: DomesticSpecialModule[] = [];
|
const modules: DomesticTraitModule[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (seen.has(key)) {
|
if (seen.has(key)) {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from './types.js';
|
|||||||
export * from './registry.js';
|
export * from './registry.js';
|
||||||
export * from './domestic/index.js';
|
export * from './domestic/index.js';
|
||||||
export * from './war/index.js';
|
export * from './war/index.js';
|
||||||
|
export * from './personality/index.js';
|
||||||
|
|||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule['onCalcStat'], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModule["onCalcStat"], undefined>,
|
||||||
|
};
|
||||||
@@ -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<TraitModuleExport>;
|
||||||
|
|
||||||
|
const defaultImporters: Record<
|
||||||
|
PersonalityTraitKey,
|
||||||
|
PersonalityTraitImporter
|
||||||
|
> = {
|
||||||
|
che_안전: async () => import('./che_안전.js'),
|
||||||
|
che_유지: async () => import('./che_유지.js'),
|
||||||
|
che_재간: async () => import('./che_재간.js'),
|
||||||
|
che_출세: async () => import('./che_출세.js'),
|
||||||
|
che_할거: async () => import('./che_할거.js'),
|
||||||
|
che_정복: async () => import('./che_정복.js'),
|
||||||
|
che_패권: async () => import('./che_패권.js'),
|
||||||
|
che_의협: async () => import('./che_의협.js'),
|
||||||
|
che_대의: async () => import('./che_대의.js'),
|
||||||
|
che_왕좌: async () => import('./che_왕좌.js'),
|
||||||
|
che_은둔: async () => import('./che_은둔.js'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPersonalityTraitKey = (
|
||||||
|
value: string
|
||||||
|
): value is PersonalityTraitKey =>
|
||||||
|
PERSONALITY_TRAIT_KEYS.includes(value as PersonalityTraitKey);
|
||||||
|
|
||||||
|
export class PersonalityTraitLoader {
|
||||||
|
private readonly cache = new Map<
|
||||||
|
PersonalityTraitKey,
|
||||||
|
Promise<PersonalityTraitModule>
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly importers: Record<
|
||||||
|
PersonalityTraitKey,
|
||||||
|
PersonalityTraitImporter
|
||||||
|
> = defaultImporters
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async load(key: PersonalityTraitKey): Promise<PersonalityTraitModule> {
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const importer = this.importers[key];
|
||||||
|
if (!importer) {
|
||||||
|
throw new Error(`Unknown personality trait key: ${key}`);
|
||||||
|
}
|
||||||
|
const loading = importer().then((module) => {
|
||||||
|
if (!('traitModule' in module)) {
|
||||||
|
throw new Error(`Missing traitModule for personality trait: ${key}`);
|
||||||
|
}
|
||||||
|
const resolved = module.traitModule;
|
||||||
|
if (resolved.key !== key) {
|
||||||
|
throw new Error(
|
||||||
|
`Personality trait key mismatch: expected ${key}, got ${resolved.key}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (resolved.kind !== 'personality') {
|
||||||
|
throw new Error(
|
||||||
|
`Personality trait kind mismatch: ${resolved.key}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
});
|
||||||
|
this.cache.set(key, loading);
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadPersonalityTraitModules = async (
|
||||||
|
keys: PersonalityTraitKey[],
|
||||||
|
loader: PersonalityTraitLoader = new PersonalityTraitLoader()
|
||||||
|
): Promise<PersonalityTraitModule[]> => {
|
||||||
|
const modules: PersonalityTraitModule[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const key of keys) {
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
modules.push(await loader.load(key));
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
};
|
||||||
@@ -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 { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||||
import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||||
import type {
|
import type {
|
||||||
SpecialActionKind,
|
TraitKind,
|
||||||
SpecialActionModule,
|
TraitModule,
|
||||||
SpecialActionModuleRegistry,
|
TraitModuleRegistry,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
const resolveSpecialKey = (
|
const resolveTraitKey = (
|
||||||
context: { general: { role: { specialDomestic: string | null; specialWar: string | null } } },
|
context: {
|
||||||
kind: SpecialActionKind
|
general: {
|
||||||
): string | null =>
|
role: {
|
||||||
kind === 'domestic'
|
personality: string | null;
|
||||||
? context.general.role.specialDomestic
|
specialDomestic: string | null;
|
||||||
: context.general.role.specialWar;
|
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 = <
|
const resolveModule = <TriggerState extends GeneralTriggerState>(
|
||||||
TriggerState extends GeneralTriggerState
|
registry: TraitModuleRegistry<TriggerState>,
|
||||||
>(
|
kind: TraitKind,
|
||||||
registry: SpecialActionModuleRegistry<TriggerState>,
|
|
||||||
kind: SpecialActionKind,
|
|
||||||
key: string | null
|
key: string | null
|
||||||
): SpecialActionModule<TriggerState> | null => {
|
): TraitModule<TriggerState> | null => {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const bucket = kind === 'domestic' ? registry.domestic : registry.war;
|
let bucket: Map<string, TraitModule<TriggerState>>;
|
||||||
|
if (kind === 'domestic') {
|
||||||
|
bucket = registry.domestic;
|
||||||
|
} else if (kind === 'war') {
|
||||||
|
bucket = registry.war;
|
||||||
|
} else {
|
||||||
|
bucket = registry.personality;
|
||||||
|
}
|
||||||
return bucket.get(key) ?? null;
|
return bucket.get(key) ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// General 파이프라인에서 특기 모듈을 선택해 위임하는 라우터.
|
// General 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
|
||||||
export class SpecialGeneralActionRouter<
|
export class TraitGeneralActionRouter<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> implements GeneralActionModule<TriggerState> {
|
> implements GeneralActionModule<TriggerState> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly kind: SpecialActionKind,
|
private readonly kind: TraitKind,
|
||||||
private readonly registry: SpecialActionModuleRegistry<TriggerState>
|
private readonly registry: TraitModuleRegistry<TriggerState>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getModule(
|
private getModule(
|
||||||
context: GeneralActionContext<TriggerState>
|
context: GeneralActionContext<TriggerState>
|
||||||
): SpecialActionModule<TriggerState> | null {
|
): TraitModule<TriggerState> | null {
|
||||||
const key = resolveSpecialKey(context, this.kind);
|
const key = resolveTraitKey(context, this.kind);
|
||||||
return resolveModule(this.registry, this.kind, key);
|
return resolveModule(this.registry, this.kind, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,19 +153,19 @@ export class SpecialGeneralActionRouter<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전투 파이프라인에서 특기 모듈을 선택해 위임하는 라우터.
|
// 전투 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
|
||||||
export class SpecialWarActionRouter<
|
export class TraitWarActionRouter<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> implements WarActionModule<TriggerState> {
|
> implements WarActionModule<TriggerState> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly kind: SpecialActionKind,
|
private readonly kind: TraitKind,
|
||||||
private readonly registry: SpecialActionModuleRegistry<TriggerState>
|
private readonly registry: TraitModuleRegistry<TriggerState>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getModule(
|
private getModule(
|
||||||
context: WarActionContext<TriggerState>
|
context: WarActionContext<TriggerState>
|
||||||
): SpecialActionModule<TriggerState> | null {
|
): TraitModule<TriggerState> | null {
|
||||||
const key = resolveSpecialKey(context, this.kind);
|
const key = resolveTraitKey(context, this.kind);
|
||||||
return resolveModule(this.registry, this.kind, key);
|
return resolveModule(this.registry, this.kind, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,21 +213,23 @@ export class SpecialWarActionRouter<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialActionModuleSet<
|
export interface TraitModuleSet<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> {
|
> {
|
||||||
general: GeneralActionModule<TriggerState>[];
|
general: GeneralActionModule<TriggerState>[];
|
||||||
war: WarActionModule<TriggerState>[];
|
war: WarActionModule<TriggerState>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSpecialActionModuleRegistry = <
|
export const createTraitModuleRegistry = <
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
>(options: {
|
>(options: {
|
||||||
domestic?: SpecialActionModule<TriggerState>[];
|
domestic?: TraitModule<TriggerState>[];
|
||||||
war?: SpecialActionModule<TriggerState>[];
|
war?: TraitModule<TriggerState>[];
|
||||||
}): SpecialActionModuleRegistry<TriggerState> => {
|
personality?: TraitModule<TriggerState>[];
|
||||||
const domestic = new Map<string, SpecialActionModule<TriggerState>>();
|
}): TraitModuleRegistry<TriggerState> => {
|
||||||
const war = new Map<string, SpecialActionModule<TriggerState>>();
|
const domestic = new Map<string, TraitModule<TriggerState>>();
|
||||||
|
const war = new Map<string, TraitModule<TriggerState>>();
|
||||||
|
const personality = new Map<string, TraitModule<TriggerState>>();
|
||||||
|
|
||||||
for (const module of options.domestic ?? []) {
|
for (const module of options.domestic ?? []) {
|
||||||
domestic.set(module.key, module);
|
domestic.set(module.key, module);
|
||||||
@@ -217,22 +237,27 @@ export const createSpecialActionModuleRegistry = <
|
|||||||
for (const module of options.war ?? []) {
|
for (const module of options.war ?? []) {
|
||||||
war.set(module.key, module);
|
war.set(module.key, module);
|
||||||
}
|
}
|
||||||
|
for (const module of options.personality ?? []) {
|
||||||
|
personality.set(module.key, module);
|
||||||
|
}
|
||||||
|
|
||||||
return { domestic, war };
|
return { domestic, war, personality };
|
||||||
};
|
};
|
||||||
|
|
||||||
// 특기 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다.
|
// 특성 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다.
|
||||||
export const createSpecialActionModules = <
|
export const createTraitModules = <
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
>(
|
>(
|
||||||
registry: SpecialActionModuleRegistry<TriggerState>
|
registry: TraitModuleRegistry<TriggerState>
|
||||||
): SpecialActionModuleSet<TriggerState> => ({
|
): TraitModuleSet<TriggerState> => ({
|
||||||
general: [
|
general: [
|
||||||
new SpecialGeneralActionRouter<TriggerState>('domestic', registry),
|
new TraitGeneralActionRouter<TriggerState>('domestic', registry),
|
||||||
new SpecialGeneralActionRouter<TriggerState>('war', registry),
|
new TraitGeneralActionRouter<TriggerState>('war', registry),
|
||||||
|
new TraitGeneralActionRouter<TriggerState>('personality', registry),
|
||||||
],
|
],
|
||||||
war: [
|
war: [
|
||||||
new SpecialWarActionRouter<TriggerState>('domestic', registry),
|
new TraitWarActionRouter<TriggerState>('domestic', registry),
|
||||||
new SpecialWarActionRouter<TriggerState>('war', registry),
|
new TraitWarActionRouter<TriggerState>('war', registry),
|
||||||
|
new TraitWarActionRouter<TriggerState>('personality', registry),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.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;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
info: string;
|
info: string;
|
||||||
kind: SpecialActionKind;
|
kind: TraitKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SpecialActionModule<
|
export type TraitModule<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> = SpecialActionSpec &
|
> = TraitSpec &
|
||||||
GeneralActionModule<TriggerState> &
|
GeneralActionModule<TriggerState> &
|
||||||
WarActionModule<TriggerState>;
|
WarActionModule<TriggerState>;
|
||||||
|
|
||||||
export interface SpecialActionModuleExport<
|
export interface TraitModuleExport<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> {
|
> {
|
||||||
specialModule: SpecialActionModule<TriggerState>;
|
traitModule: TraitModule<TriggerState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialActionModuleRegistry<
|
export interface TraitModuleRegistry<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
> {
|
> {
|
||||||
domestic: Map<string, SpecialActionModule<TriggerState>>;
|
domestic: Map<string, TraitModule<TriggerState>>;
|
||||||
war: Map<string, SpecialActionModule<TriggerState>>;
|
war: Map<string, TraitModule<TriggerState>>;
|
||||||
|
personality: Map<string, TraitModule<TriggerState>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
|
|||||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||||
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
|
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
|
||||||
import { triggerModule as cheUisulTriggerModule } from '@sammo-ts/logic/war/triggers/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_의술',
|
key: 'che_의술',
|
||||||
name: '의술',
|
name: '의술',
|
||||||
info: '[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)',
|
info: '[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||||
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
|
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
|
||||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.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 RECRUIT_TRAIN = 70;
|
||||||
const CONSCRIPT_TRAIN = 84;
|
const CONSCRIPT_TRAIN = 84;
|
||||||
@@ -41,7 +41,7 @@ function onCalcStat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 전투 특기: 징병
|
// 전투 특기: 징병
|
||||||
export const specialModule: SpecialActionModule = {
|
export const traitModule: TraitModule = {
|
||||||
key: 'che_징병',
|
key: 'che_징병',
|
||||||
name: '징병',
|
name: '징병',
|
||||||
info: '[군사] 징병/모병 시 훈사 70/84 제공<br>[기타] 통솔 순수 능력치 보정 +25%, 징병/모병/소집해제 시 인구 변동 없음',
|
info: '[군사] 징병/모병 시 훈사 70/84 제공<br>[기타] 통솔 순수 능력치 보정 +25%, 징병/모병/소집해제 시 인구 변동 없음',
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
import type {
|
import type {
|
||||||
SpecialActionModule,
|
TraitModule,
|
||||||
SpecialActionModuleExport,
|
TraitModuleExport,
|
||||||
} from '@sammo-ts/logic/triggers/special/types.js';
|
} from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
|
|
||||||
export const WAR_SPECIAL_KEYS = [
|
export const WAR_TRAIT_KEYS = [
|
||||||
'che_의술',
|
'che_의술',
|
||||||
'che_징병',
|
'che_징병',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type WarSpecialKey =
|
export type WarTraitKey =
|
||||||
(typeof WAR_SPECIAL_KEYS)[number];
|
(typeof WAR_TRAIT_KEYS)[number];
|
||||||
|
|
||||||
export type WarSpecialModule = SpecialActionModule;
|
export type WarTraitModule = TraitModule;
|
||||||
|
|
||||||
export type WarSpecialImporter = () => Promise<SpecialActionModuleExport>;
|
export type WarTraitImporter = () => Promise<TraitModuleExport>;
|
||||||
|
|
||||||
const defaultImporters: Record<
|
const defaultImporters: Record<
|
||||||
WarSpecialKey,
|
WarTraitKey,
|
||||||
WarSpecialImporter
|
WarTraitImporter
|
||||||
> = {
|
> = {
|
||||||
che_의술: async () => import('./che_의술.js'),
|
che_의술: async () => import('./che_의술.js'),
|
||||||
che_징병: async () => import('./che_징병.js'),
|
che_징병: async () => import('./che_징병.js'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isWarSpecialKey = (
|
export const isWarTraitKey = (
|
||||||
value: string
|
value: string
|
||||||
): value is WarSpecialKey =>
|
): value is WarTraitKey =>
|
||||||
WAR_SPECIAL_KEYS.includes(value as WarSpecialKey);
|
WAR_TRAIT_KEYS.includes(value as WarTraitKey);
|
||||||
|
|
||||||
export class WarSpecialLoader {
|
export class WarTraitLoader {
|
||||||
private readonly cache = new Map<
|
private readonly cache = new Map<
|
||||||
WarSpecialKey,
|
WarTraitKey,
|
||||||
Promise<WarSpecialModule>
|
Promise<WarTraitModule>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly importers: Record<
|
private readonly importers: Record<
|
||||||
WarSpecialKey,
|
WarTraitKey,
|
||||||
WarSpecialImporter
|
WarTraitImporter
|
||||||
> = defaultImporters
|
> = defaultImporters
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async load(key: WarSpecialKey): Promise<WarSpecialModule> {
|
async load(key: WarTraitKey): Promise<WarTraitModule> {
|
||||||
const cached = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
const importer = this.importers[key];
|
const importer = this.importers[key];
|
||||||
if (!importer) {
|
if (!importer) {
|
||||||
throw new Error(`Unknown war special key: ${key}`);
|
throw new Error(`Unknown war trait key: ${key}`);
|
||||||
}
|
}
|
||||||
const loading = importer().then((module) => {
|
const loading = importer().then((module) => {
|
||||||
if (!('specialModule' in module)) {
|
if (!('traitModule' in module)) {
|
||||||
throw new Error(`Missing specialModule for war special: ${key}`);
|
throw new Error(`Missing traitModule for war trait: ${key}`);
|
||||||
}
|
}
|
||||||
const resolved = module.specialModule;
|
const resolved = module.traitModule;
|
||||||
if (resolved.key !== key) {
|
if (resolved.key !== key) {
|
||||||
throw new Error(
|
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') {
|
if (resolved.kind !== 'war') {
|
||||||
throw new Error(`War special kind mismatch: ${resolved.key}`);
|
throw new Error(`War trait kind mismatch: ${resolved.key}`);
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
});
|
});
|
||||||
@@ -70,11 +70,11 @@ export class WarSpecialLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadWarSpecialModules = async (
|
export const loadWarTraitModules = async (
|
||||||
keys: WarSpecialKey[],
|
keys: WarTraitKey[],
|
||||||
loader: WarSpecialLoader = new WarSpecialLoader()
|
loader: WarTraitLoader = new WarTraitLoader()
|
||||||
): Promise<WarSpecialModule[]> => {
|
): Promise<WarTraitModule[]> => {
|
||||||
const modules: WarSpecialModule[] = [];
|
const modules: WarTraitModule[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (seen.has(key)) {
|
if (seen.has(key)) {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export type TriggerDomesticActionType =
|
|||||||
| '민심'
|
| '민심'
|
||||||
| '인구'
|
| '인구'
|
||||||
| '기술'
|
| '기술'
|
||||||
| '모병';
|
| '모병'
|
||||||
|
| '단련';
|
||||||
|
|
||||||
export type TriggerDomesticVarType =
|
export type TriggerDomesticVarType =
|
||||||
| 'cost'
|
| 'cost'
|
||||||
@@ -36,7 +37,12 @@ export type TriggerStrategicVarType = 'delay' | 'globalDelay';
|
|||||||
|
|
||||||
export type TriggerNationalIncomeType = 'gold' | 'rice';
|
export type TriggerNationalIncomeType = 'gold' | 'rice';
|
||||||
|
|
||||||
export type GeneralStatName = 'leadership' | 'strength' | 'intelligence';
|
export type GeneralStatName =
|
||||||
|
| 'leadership'
|
||||||
|
| 'strength'
|
||||||
|
| 'intelligence'
|
||||||
|
| 'experience'
|
||||||
|
| 'dedication';
|
||||||
|
|
||||||
export type WarStatName =
|
export type WarStatName =
|
||||||
| GeneralStatName
|
| GeneralStatName
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import type { UnitSetDefinition } from '../src/world/types.js';
|
|||||||
import { GeneralActionPipeline } from '../src/triggers/general-action.js';
|
import { GeneralActionPipeline } from '../src/triggers/general-action.js';
|
||||||
import { createGeneralTriggerContext } from '../src/triggers/general.js';
|
import { createGeneralTriggerContext } from '../src/triggers/general.js';
|
||||||
import {
|
import {
|
||||||
createSpecialActionModuleRegistry,
|
createTraitModuleRegistry,
|
||||||
createSpecialActionModules,
|
createTraitModules,
|
||||||
loadDomesticSpecialModules,
|
loadDomesticTraitModules,
|
||||||
loadWarSpecialModules,
|
loadWarTraitModules,
|
||||||
} from '../src/triggers/special/index.js';
|
} from '../src/triggers/special/index.js';
|
||||||
import { ActionLogger } from '../src/logging/actionLogger.js';
|
import { ActionLogger } from '../src/logging/actionLogger.js';
|
||||||
import { WarActionPipeline } from '../src/war/actions.js';
|
import { WarActionPipeline } from '../src/war/actions.js';
|
||||||
@@ -161,13 +161,13 @@ const buildUnitSet = (): UnitSetDefinition => ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('special action modules', () => {
|
describe('trait modules', () => {
|
||||||
it('loads special modules by key', async () => {
|
it('loads trait modules by key', async () => {
|
||||||
const domestic = await loadDomesticSpecialModules([
|
const domestic = await loadDomesticTraitModules([
|
||||||
'che_인덕',
|
'che_인덕',
|
||||||
'che_발명',
|
'che_발명',
|
||||||
]);
|
]);
|
||||||
const war = await loadWarSpecialModules(['che_의술', 'che_징병']);
|
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
|
||||||
|
|
||||||
expect(domestic.map((module) => module.key)).toEqual([
|
expect(domestic.map((module) => module.key)).toEqual([
|
||||||
'che_인덕',
|
'che_인덕',
|
||||||
@@ -180,15 +180,15 @@ describe('special action modules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies domestic and war modifiers in general pipeline', async () => {
|
it('applies domestic and war modifiers in general pipeline', async () => {
|
||||||
const domestic = await loadDomesticSpecialModules([
|
const domestic = await loadDomesticTraitModules([
|
||||||
'che_인덕',
|
'che_인덕',
|
||||||
'che_발명',
|
'che_발명',
|
||||||
]);
|
]);
|
||||||
const war = await loadWarSpecialModules(['che_의술', 'che_징병']);
|
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
|
||||||
const registry = createSpecialActionModuleRegistry({ domestic, war });
|
const registry = createTraitModuleRegistry({ domestic, war });
|
||||||
const specialModules = createSpecialActionModules(registry);
|
const traitModules = createTraitModules(registry);
|
||||||
|
|
||||||
const pipeline = new GeneralActionPipeline(specialModules.general);
|
const pipeline = new GeneralActionPipeline(traitModules.general);
|
||||||
const general = buildGeneral({
|
const general = buildGeneral({
|
||||||
role: {
|
role: {
|
||||||
personality: null,
|
personality: null,
|
||||||
@@ -214,14 +214,14 @@ describe('special action modules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('heals city generals with 의술 pre-turn trigger', async () => {
|
it('heals city generals with 의술 pre-turn trigger', async () => {
|
||||||
const domestic = await loadDomesticSpecialModules([
|
const domestic = await loadDomesticTraitModules([
|
||||||
'che_인덕',
|
'che_인덕',
|
||||||
'che_발명',
|
'che_발명',
|
||||||
]);
|
]);
|
||||||
const war = await loadWarSpecialModules(['che_의술', 'che_징병']);
|
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
|
||||||
const registry = createSpecialActionModuleRegistry({ domestic, war });
|
const registry = createTraitModuleRegistry({ domestic, war });
|
||||||
const specialModules = createSpecialActionModules(registry);
|
const traitModules = createTraitModules(registry);
|
||||||
const pipeline = new GeneralActionPipeline(specialModules.general);
|
const pipeline = new GeneralActionPipeline(traitModules.general);
|
||||||
|
|
||||||
const general = buildGeneral({
|
const general = buildGeneral({
|
||||||
injury: 20,
|
injury: 20,
|
||||||
@@ -270,13 +270,13 @@ describe('special action modules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('activates 의술 battle trigger and reduces damage', async () => {
|
it('activates 의술 battle trigger and reduces damage', async () => {
|
||||||
const domestic = await loadDomesticSpecialModules([
|
const domestic = await loadDomesticTraitModules([
|
||||||
'che_인덕',
|
'che_인덕',
|
||||||
'che_발명',
|
'che_발명',
|
||||||
]);
|
]);
|
||||||
const war = await loadWarSpecialModules(['che_의술', 'che_징병']);
|
const war = await loadWarTraitModules(['che_의술', 'che_징병']);
|
||||||
const registry = createSpecialActionModuleRegistry({ domestic, war });
|
const registry = createTraitModuleRegistry({ domestic, war });
|
||||||
const specialModules = createSpecialActionModules(registry);
|
const traitModules = createTraitModules(registry);
|
||||||
|
|
||||||
const rng = new RandUtil(new ConstantRNG(0));
|
const rng = new RandUtil(new ConstantRNG(0));
|
||||||
const config = buildConfig();
|
const config = buildConfig();
|
||||||
@@ -308,7 +308,7 @@ describe('special action modules', () => {
|
|||||||
true,
|
true,
|
||||||
crewType,
|
crewType,
|
||||||
new ActionLogger({ generalId: 1, nationId: 1 }),
|
new ActionLogger({ generalId: 1, nationId: 1 }),
|
||||||
new WarActionPipeline(specialModules.war)
|
new WarActionPipeline(traitModules.war)
|
||||||
);
|
);
|
||||||
const defender = new WarUnitCity(
|
const defender = new WarUnitCity(
|
||||||
rng,
|
rng,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
tsconfigPaths({
|
tsconfigPaths({
|
||||||
projects: [path.resolve(__dirname, '../../tsconfig.base.json')],
|
projects: [path.resolve(__dirname, '../../tsconfig.paths.json')],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
Reference in New Issue
Block a user