refactor(logic): replace arbitrary action hooks with typed events

This commit is contained in:
2026-07-30 03:31:31 +00:00
parent 96b41ef4b4
commit 7d3cb50a5b
168 changed files with 1554 additions and 631 deletions
+9 -1
View File
@@ -60,7 +60,7 @@ commit 또는 삭제를 하지 말아 주세요.
- `app/game-api`: tRPC/SSE, 조회·입력 API와 battle/auction/tournament worker
- `app/game-engine`: turn daemon, scheduler, 월간 lifecycle와 DB flush
- `packages/common`: 타입, 직렬화, RNG와 공통 유틸리티
- `packages/logic`: 전투·명령·월간 action 도메인 로직
- `packages/logic`: 전투·명령·월간 action과 typed action module 도메인 로직
- `packages/infra`: gateway/game Prisma schema, migration과 client
- `tools/integration-tests`: PostgreSQL/Redis 및 ref↔core 차등
- `tools/frontend-legacy-parity`: 실제 Chromium 비교
@@ -71,6 +71,14 @@ placeholder입니다. 이를 완성된 배포 bundle이나 검증된 profile bui
않습니다. 운영 build/reset/open은 gateway operation, commit별 worktree와
orchestrator 실행 경로를 조사해 주세요.
장수 action은 `packages/logic/src/actionModules/`에 둡니다. 계산 fold,
priority/unique-ID trigger와 닫힌 의미 이벤트를 한 범용 hook으로 합치지
말아 주세요. 제품용 module 순서는 `loadActionModuleBundle()`
`RefOrderedActionStack`에서만 조립하며, 새 의미 이벤트는
`GeneralActionEventPayloadMap`에 payload와 필수 context를 먼저 선언해
주세요. 자세한 계약은 `docs/architecture/action-module-protocol.md`
있습니다.
## 레거시 매핑과 비교
기능을 변경하기 전에 다음을 end-to-end로 연결해 주세요.
+7 -1
View File
@@ -35,7 +35,7 @@ TypeScript로 호환 이관하는 pnpm 모노레포입니다. 기준 구현은
| `app/game-api` | profile별 tRPC/SSE, 조회·입력 API와 비동기 worker |
| `app/game-engine` | 턴 scheduler/daemon, 월간 처리와 DB flush |
| `packages/common` | 공통 타입, 직렬화, 결정적 RNG와 유틸리티 |
| `packages/logic` | 전투·명령·월간 action 등 게임 도메인 로직 |
| `packages/logic` | 전투·명령·월간 action과 typed action module 로직 |
| `packages/infra` | game/gateway Prisma schema, migration과 client |
| `packages/tools-scripts` | resource schema 생성·검증 도구 |
| `tools/integration-tests` | PostgreSQL/Redis 및 ref↔core 통합·차등 테스트 |
@@ -53,6 +53,12 @@ battle simulation 등 해당 기능의 계약에만 사용하며 게임 mutation
[`docs/architecture/turn-daemon-lifecycle.md`](docs/architecture/turn-daemon-lifecycle.md)에
있습니다.
장수 특기·관직·병종·계승·아이템 효과는
[`docs/architecture/action-module-protocol.md`](docs/architecture/action-module-protocol.md)의
계산 hook, 우선순위 trigger, 닫힌 의미 이벤트 경계를 따릅니다. 제품용
action stack은 ref의 소유권 순서를 brand한 `loadActionModuleBundle()`에서만
조립합니다.
## 도구 체인
- pnpm workspace와 Turbo
+21 -11
View File
@@ -11,10 +11,11 @@ import {
resolveWarBattle,
createItemActionModules,
createItemModuleRegistry,
createRefOrderedActionStack,
ITEM_KEYS,
loadItemModules,
createInheritBuffModules,
createTraitModuleRegistry,
createTraitCatalog,
createOfficerLevelActionModules,
DOMESTIC_TRAIT_KEYS,
loadDomesticTraitModules,
@@ -30,6 +31,7 @@ import {
type City,
type General,
type Nation,
type RefOrderedActionStack,
type UnitSetDefinition,
type WarBattleOutcome,
type WarActionModule,
@@ -48,23 +50,31 @@ const itemWarModules: WarActionModule[] = createItemActionModules(
createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]))
).war;
const crewTypeWarTriggerRegistry = createCrewTypeWarTriggerRegistry();
const traitRegistry = createTraitModuleRegistry({
const traitCatalog = createTraitCatalog({
domestic: await loadDomesticTraitModules([...DOMESTIC_TRAIT_KEYS]),
war: await loadWarTraitModules([...WAR_TRAIT_KEYS]),
personality: await loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
nation: await loadNationTraitModules([...NATION_TRAIT_KEYS]),
});
const traitWarModules: WarActionModule[] = [
new TraitWarActionRouter('nation', traitRegistry),
createOfficerLevelActionModules().war,
new TraitWarActionRouter('domestic', traitRegistry),
new TraitWarActionRouter('war', traitRegistry),
new TraitWarActionRouter('personality', traitRegistry),
];
const nationWarModule = new TraitWarActionRouter('nation', traitCatalog);
const officerWarModule = createOfficerLevelActionModules().war;
const domesticWarModule = new TraitWarActionRouter('domestic', traitCatalog);
const warTraitModule = new TraitWarActionRouter('war', traitCatalog);
const personalityWarModule = new TraitWarActionRouter('personality', traitCatalog);
const buildWarActionModules = (unitSet: UnitSetDefinition): WarActionModule[] => {
const buildWarActionModules = (unitSet: UnitSetDefinition): RefOrderedActionStack<WarActionModule> => {
const crewTypeCatalog = compileCrewTypeCatalog(unitSet, crewTypeWarTriggerRegistry);
return [...traitWarModules, crewTypeCatalog.warActionModule, inheritBuffModules.war, ...itemWarModules];
return createRefOrderedActionStack<WarActionModule>({
nation: nationWarModule,
officer: officerWarModule,
domestic: domesticWarModule,
war: warTraitModule,
personality: personalityWarModule,
crewType: crewTypeCatalog.warActionModule,
inheritance: inheritBuffModules.war,
scenario: null,
items: itemWarModules,
});
};
const normalizeItemCode = (value: string | null): string | null => (value === 'None' ? null : value);
@@ -22,7 +22,11 @@ const DISASTER_TEXT_BY_MONTH: Readonly<Record<number, DisasterText[]>> = {
1: [
{ title: '<M><b>【재난】</b></>', stateCode: 4, body: '역병이 발생하여 도시가 황폐해지고 있습니다.' },
{ title: '<M><b>【재난】</b></>', stateCode: 5, body: '지진으로 피해가 속출하고 있습니다.' },
{ title: '<M><b>【재난】</b></>', stateCode: 3, body: '추위가 풀리지 않아 얼어죽는 백성들이 늘어나고 있습니다.' },
{
title: '<M><b>【재난】</b></>',
stateCode: 3,
body: '추위가 풀리지 않아 얼어죽는 백성들이 늘어나고 있습니다.',
},
{ title: '<M><b>【재난】</b></>', stateCode: 9, body: '황건적이 출현해 도시를 습격하고 있습니다.' },
],
4: [
@@ -67,7 +71,7 @@ const roundLegacyIntegerColumn = (value: number): number => Math.round(value);
export const createRaiseDisasterHandler = (options: {
getWorld: () => InMemoryTurnWorld | null;
generalActionModules?: GeneralActionModule[];
generalActionModules?: ReadonlyArray<GeneralActionModule>;
}): MonthlyEventActionHandler => {
const generalPipeline = new GeneralActionPipeline(options.generalActionModules ?? []);
@@ -95,9 +99,7 @@ export const createRaiseDisasterHandler = (options: {
throw new Error(`Unsupported month for RaiseDisaster: ${environment.month}`);
}
const rng = new RandUtil(
new LiteHashDRBG(
simpleSerialize(resolveHiddenSeed(world), 'disater', environment.year, environment.month)
)
new LiteHashDRBG(simpleSerialize(resolveHiddenSeed(world), 'disater', environment.year, environment.month))
);
const isGood = rng.nextBool(boomingRate);
const targetCities = cities.filter((city) => {
@@ -135,9 +137,7 @@ export const createRaiseDisasterHandler = (options: {
world.updateCity(city.id, {
state: picked.stateCode,
population: roundLegacyIntegerColumn(
isGood
? Math.min(city.population * affectRatio, city.populationMax)
: city.population * affectRatio
isGood ? Math.min(city.population * affectRatio, city.populationMax) : city.population * affectRatio
),
agriculture: roundLegacyIntegerColumn(
isGood
@@ -171,8 +171,7 @@ export const createRaiseDisasterHandler = (options: {
nation: world.getNationById(general.nationId),
worldView: {
listGenerals: () => allGenerals,
listGeneralsByCity: (cityId) =>
allGenerals.filter((candidate) => candidate.cityId === cityId),
listGeneralsByCity: (cityId) => allGenerals.filter((candidate) => candidate.cityId === cityId),
},
rng,
});
@@ -186,8 +186,8 @@ export const buildReservedTurnDefinitions = async (options: {
},
])
);
options.env.generalActionModules = [...(options.env.generalActionModules ?? []), ...moduleBundle.general];
options.env.warActionModules = [...(options.env.warActionModules ?? []), ...moduleBundle.war];
options.env.generalActionModules ??= moduleBundle.general;
options.env.warActionModules ??= moduleBundle.war;
options.env.nationTraitModules = moduleBundle.nationTraitModules;
const generalSpecs = await loadGeneralTurnCommandSpecs(options.commandProfile.general);
@@ -976,6 +976,11 @@ export const createReservedTurnHandler = async (options: {
getAdditionalOccupiedUniqueItemKeys: options.getAdditionalOccupiedUniqueItemKeys,
});
let actionRng = sharedActionRng ?? buildRng(actionKey);
const actionTime = {
year: context.world.currentYear,
month: context.world.currentMonth,
startYear: resolveStartYear(context.world, options.scenarioMeta),
};
let baseContext: ActionContextBase = {
general: currentGeneral,
city: currentCity,
@@ -991,6 +996,7 @@ export const createReservedTurnHandler = async (options: {
}
: {}),
rng: actionRng,
time: actionTime,
uniqueLottery,
};
let specificContext = buildActionContext(
@@ -1023,6 +1029,7 @@ export const createReservedTurnHandler = async (options: {
city: currentCity,
nation: currentNation,
rng: actionRng,
time: actionTime,
};
specificContext = baseContext;
}
@@ -0,0 +1,84 @@
# 장수 행동 모듈 프로토콜
`packages/logic/src/actionModules/`는 ref의 `iAction`을 그대로 복사한 범용
interface가 아니라, 실제 core2026 실행 경계만 타입으로 표현합니다. 계산
hook, 우선순위 trigger, 의미 이벤트는 서로 다른 실행 계약입니다.
## 세 가지 실행 계약
| 계약 | core2026 경계 | 실행 의미 |
| ---------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
| 순차 계산 hook | `GeneralActionPipeline` | ref의 `General::getActionList()` 순서대로 값을 fold합니다. |
| 우선순위 trigger | `triggers/core.ts`, `triggers/general.ts`, 전투 trigger | priority, 삽입 순서와 unique ID 중복 제거를 보존한 뒤 fire합니다. |
| 의미 이벤트 | `actionModules/events.ts` | 닫힌 이벤트별 payload와 context를 동기적으로 순회합니다. |
priority trigger를 의미 이벤트로 바꾸거나, 의미 이벤트를 `TriggerCaller`
감싸지 않습니다. 두 경로는 정렬과 중복 제거 의미가 다릅니다.
## ref 순서와 소유권
정기턴은 `loadActionModuleBundle()`에서, 전투 시뮬레이터는 같은
`createRefOrderedActionStack()` factory에서 제품용 action stack을
조립합니다. 순서는 다음과 같습니다.
1. 국가 타입
2. 관직
3. 내정 특기
4. 전투 특기
5. 성격
6. 병종
7. 계승 버프
8. 시나리오 효과
9. 아이템
`RefOrderedActionStack`의 readonly unique-symbol brand는 임의 배열을 제품용
표준 stack으로 오인하지 않게 하는 shadow type입니다. 모든 slot을 명시하는
factory에서만 이 brand를 만들 수 있으며, 예약턴 runtime env에도 spread하지
않고 그대로 전달합니다. 현재 시나리오 효과 runtime module은 이식되지 않아
해당 slot은 명시적으로 `null`입니다. 따라서 이 brand는 순서와 slot 소유권을
증명하며, 시나리오 효과 구현 완료를 뜻하지 않습니다.
## 닫힌 의미 이벤트
`GeneralActionEventPayloadMap`이 허용하는 이벤트와 payload의 단일
source입니다. 현재 이벤트는 장비 구매·판매, 계략 성공, 도시 점령입니다.
- 이벤트는 `createGeneralActionEvent()`만 생성합니다. private
unique-symbol brand 때문에 객체 literal로 위조할 수 없습니다.
- `GeneralActionEventContext<K>`가 이벤트별 필수 능력을 정합니다. 예를 들어
판매는 RNG와 연월, 도시 점령은 RNG가 없으면 compile되지 않습니다.
- leaf module은 `eventHandlers`에 처리하는 이벤트 key만 선언합니다.
- trait, 병종과 item catalog 같은 합성 router만 내부 `handleEvent`
구현합니다. 두 capability는 `never`를 사용한 상호 배타적 union이라 한
module에서 동시에 선언할 수 없습니다.
- 새 문자열 action name, `phase`, `aux: Record<string, unknown>`를 범용
우회로로 추가하지 않습니다.
새 이벤트를 추가할 때는 payload map과 context 조건을 먼저 추가한 뒤,
실제 producer와 필요한 handler만 연결합니다. 존재하지 않는 handler
종류를 interface에 선행 추가하지 않습니다.
## 저장과 RNG 경계
의미 이벤트는 producer가 가진 객체를 동기적으로 수정합니다. producer는
이벤트 전후의 ref mutation 순서를 그대로 유지한 뒤 기존 effect/flush
경계에 결과를 전달합니다.
- 장비 판매는 판매 대금 반영 → 판매 이벤트 → 슬롯 제거 순서입니다.
- 도기 판매의 2분기는 ref `choice([gold, rice])`와 같이 index 0이 금,
index 1이 쌀입니다.
- 도시 점령은 점령 도시의 수비국 장수 전원을 입력 순서로 호출한 뒤 국가
멸망 손실을 계산합니다. 이벤트 handler와 멸망 손실은 같은
`ConquerCity` RNG 객체를 이어 씁니다.
- 계략 성공 아이템 소비는 `consumeOnStrategySuccess`라는 명시 capability로
선언하며 다른 임의 action 이름과 공유하지 않습니다.
## 검증 경계
`actionModuleEvents.test.ts`는 표준 순서, 이벤트 brand와 잘못된 context의
compile 실패를 검증합니다. `itemActionEvents.test.ts`는 도기 분기와
연차 경계, 충차·환약 초기 충전, 계략 성공 소비를 검증합니다.
`warAftermath.test.ts`는 도시 점령 대상과 공유 RNG 소비 순서를 검증합니다.
ref↔core 실제 명령 차등은
`turnCommandGeneralMatrix.integration.test.ts`의 도기 판매 fixture가
담당합니다.
@@ -200,6 +200,22 @@ non-item versions (see `ActionItem/che_저격_매화수전.php`).
per-phase activation.
- Ensure RNG usage stays deterministic (`RandUtil` everywhere in triggers).
## Current core2026 boundary
core2026 does not expose the whole legacy `iAction` surface as one interface.
Modifier folds and semantic side effects live under `src/actionModules/`;
priority callers remain under `src/triggers/` and the war trigger modules.
This separation is required because `TriggerCaller` sorts by priority and
deduplicates unique IDs, while semantic events synchronously preserve module
order and producer-owned mutation/RNG boundaries.
The former copied `onArbitraryAction(actionName, phase, aux)` surface was
removed. Equipment purchase/sale, strategy success and city conquest now use
the closed, nominally branded protocol documented in
[장수 행동 모듈 프로토콜](./action-module-protocol.md). This is a core2026
typing boundary only; the ref call order and persisted effects remain the
compatibility contract.
## Related Files
- `legacy/hwe/sammo/iAction.php`
+6 -6
View File
@@ -3,12 +3,12 @@
이 핸드북은 새 기능을 어디에 넣을지뿐 아니라 요청이 어떤 경계를 지나 상태로 남는지 설명합니다. 먼저
[문서 기준선](../reference-baseline.md)을 확인하고, 변경 성격에 따라 다음 순서로 읽어 주세요.
| 변경하려는 것 | 먼저 읽을 문서 | 주로 확인할 코드 |
| -------------------------- | ---------------------------------------------------- | -------------------------------------------- |
| 화면·라우팅·조회 API | [시스템 아키텍처](./system-architecture.md) | `app/*-frontend`, `app/*-api` |
| 턴 입력·게임 상태 mutation | [요청·턴·저장 흐름](./request-turn-persistence.md) | `app/game-api`, `app/game-engine` |
| 명령·전투·월간 로직 | [도메인 로직과 핵심 클래스](./domain-and-classes.md) | `packages/logic`, `app/game-engine/src/turn` |
| 새 파일 위치·검증 범위 | [파일 지도와 변경 절차](./code-map.md) | package manifest, test, docs |
| 변경하려는 것 | 먼저 읽을 문서 | 주로 확인할 코드 |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| 화면·라우팅·조회 API | [시스템 아키텍처](./system-architecture.md) | `app/*-frontend`, `app/*-api` |
| 턴 입력·게임 상태 mutation | [요청·턴·저장 흐름](./request-turn-persistence.md) | `app/game-api`, `app/game-engine` |
| 명령·전투·월간 로직 | [도메인 로직과 핵심 클래스](./domain-and-classes.md), [장수 행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `packages/logic`, `app/game-engine/src/turn` |
| 새 파일 위치·검증 범위 | [파일 지도와 변경 절차](./code-map.md) | package manifest, test, docs |
## 읽을 때 지켜야 할 경계
+132
View File
@@ -0,0 +1,132 @@
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.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import { createOfficerLevelActionModules } from './officerLevel.js';
import {
createTraitCatalog,
DOMESTIC_TRAIT_KEYS,
loadDomesticTraitModules,
loadNationTraitModules,
loadPersonalityTraitModules,
loadWarTraitModules,
NATION_TRAIT_KEYS,
PERSONALITY_TRAIT_KEYS,
TraitGeneralActionRouter,
TraitWarActionRouter,
WAR_TRAIT_KEYS,
} from './traits/index.js';
import type { NationTraitModule } from './traits/nation/index.js';
export interface ActionModuleBundle<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: RefOrderedActionStack<GeneralActionModule<TriggerState>>;
war: RefOrderedActionStack<WarActionModule<TriggerState>>;
itemModules: ItemModule<TriggerState>[];
nationTraitModules: NationTraitModule[];
}
const refActionOrderBrand: unique symbol = Symbol('RefOrderedActionStack');
export type RefOrderedActionStack<Module> = ReadonlyArray<Module> & {
readonly [refActionOrderBrand]: true;
};
interface RefActionSlots<Module> {
nation: Module;
officer: Module;
domestic: Module;
war: Module;
personality: Module;
crewType: Module | null;
inheritance: Module;
scenario: Module | null;
items: readonly Module[];
}
const markRefOrderedActionStack: <Module>(
stack: Module[]
) => asserts stack is Module[] & { readonly [refActionOrderBrand]: true } = (stack) => {
Object.defineProperty(stack, refActionOrderBrand, {
value: true,
enumerable: false,
writable: false,
});
};
// ref General::getActionList()의 소유권 순서를 한 곳에서만 조립합니다.
export const createRefOrderedActionStack = <Module>(slots: RefActionSlots<Module>): RefOrderedActionStack<Module> => {
const stack = [
slots.nation,
slots.officer,
slots.domestic,
slots.war,
slots.personality,
...(slots.crewType ? [slots.crewType] : []),
slots.inheritance,
...(slots.scenario ? [slots.scenario] : []),
...slots.items,
];
markRefOrderedActionStack(stack);
return stack;
};
// 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 traitCatalog = createTraitCatalog<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: createRefOrderedActionStack<GeneralActionModule<TriggerState>>({
nation: new TraitGeneralActionRouter('nation', traitCatalog),
officer: officer.general,
domestic: new TraitGeneralActionRouter('domestic', traitCatalog),
war: new TraitGeneralActionRouter('war', traitCatalog),
personality: new TraitGeneralActionRouter('personality', traitCatalog),
crewType: crewTypeCatalog
? (crewTypeCatalog.generalActionModule as GeneralActionModule<TriggerState>)
: null,
inheritance: inherit.general as GeneralActionModule<TriggerState>,
// scenarioEffect는 현재 core runtime module이 없어 명시적으로 빈 slot입니다.
scenario: null,
items: items.general,
}),
war: createRefOrderedActionStack<WarActionModule<TriggerState>>({
nation: new TraitWarActionRouter('nation', traitCatalog),
officer: officer.war,
domestic: new TraitWarActionRouter('domestic', traitCatalog),
war: new TraitWarActionRouter('war', traitCatalog),
personality: new TraitWarActionRouter('personality', traitCatalog),
crewType: crewTypeCatalog ? (crewTypeCatalog.warActionModule as WarActionModule<TriggerState>) : null,
inheritance: inherit.war as WarActionModule<TriggerState>,
// ref의 scenarioEffect 위치를 보존하되 미이식 module은 별도 gap으로 남깁니다.
scenario: null,
items: items.war,
}),
itemModules,
nationTraitModules: nation,
};
};
@@ -0,0 +1,88 @@
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { RandomGenerator } from '@sammo-ts/common';
const generalActionEventBrand: unique symbol = Symbol('GeneralActionEvent');
export interface GeneralActionEventPayloadMap {
'item.purchased': {
itemKey: string;
slot: 'horse' | 'weapon' | 'book' | 'item';
};
'item.sold': {
itemKey: string;
slot: 'horse' | 'weapon' | 'book' | 'item';
};
'strategy.succeeded': {
consumedItems: readonly string[];
};
'city.conquered': {
attacker: General;
};
}
export type GeneralActionEventType = keyof GeneralActionEventPayloadMap;
export type GeneralActionEventContext<
K extends GeneralActionEventType,
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> = K extends 'item.sold' | 'city.conquered'
? GeneralActionContext<TriggerState> & { rng: RandomGenerator } & (K extends 'item.sold'
? { time: NonNullable<GeneralActionContext<TriggerState>['time']> }
: object)
: GeneralActionContext<TriggerState>;
/**
* 레거시 문자열/phase/aux 삼중항을 닫힌 이벤트 계약으로 투영합니다.
*
* unique-symbol 필드는 런타임 분기용이 아니라 서로 다른 이벤트 payload가
* 구조적으로 우연히 호환되는 것을 막는 nominal shadow type입니다. 이벤트는
* 반드시 createGeneralActionEvent()로 만들며, handler는 같은 K만 반환합니다.
*/
export type GeneralActionEvent<
K extends GeneralActionEventType,
_TriggerState extends GeneralTriggerState = GeneralTriggerState,
> = Readonly<{
type: K;
payload: Readonly<GeneralActionEventPayloadMap[K]>;
[generalActionEventBrand]: K;
}>;
export type GeneralActionEventHandler<TriggerState extends GeneralTriggerState, K extends GeneralActionEventType> = {
bivarianceHack(
context: GeneralActionEventContext<K, TriggerState>,
event: GeneralActionEvent<K, TriggerState>
): GeneralActionEvent<K, TriggerState> | void;
}['bivarianceHack'];
export type GeneralActionEventHandlers<TriggerState extends GeneralTriggerState = GeneralTriggerState> = Partial<{
[K in GeneralActionEventType]: GeneralActionEventHandler<TriggerState, K>;
}>;
export const createGeneralActionEvent = <
TriggerState extends GeneralTriggerState = GeneralTriggerState,
K extends GeneralActionEventType = GeneralActionEventType,
>(
type: K,
payload: GeneralActionEventPayloadMap[K]
): GeneralActionEvent<K, TriggerState> => ({
type,
payload,
[generalActionEventBrand]: type,
});
export const dispatchGeneralActionEventHandlers = <
TriggerState extends GeneralTriggerState,
K extends GeneralActionEventType,
>(
handlers: GeneralActionEventHandlers<TriggerState> | null | undefined,
context: GeneralActionEventContext<K, TriggerState>,
event: GeneralActionEvent<K, TriggerState>
): GeneralActionEvent<K, TriggerState> => {
// Mapped-type lookup preserves K, but TypeScript cannot currently retain
// that correlation through a generic indexed access. This single local
// assertion is the proof boundary; module authors and call sites remain
// fully checked without unknown/any casts.
const handler = handlers?.[event.type] as GeneralActionEventHandler<TriggerState, K> | undefined;
return handler?.(context, event) ?? event;
};
@@ -1,23 +1,27 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import { type GeneralActionContext, GeneralTriggerCaller } from './general.js';
import { type GeneralActionContext, GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import type {
GeneralStatName,
TriggerActionPhase,
TriggerActionType,
TriggerDomesticActionType,
TriggerDomesticVarType,
TriggerNationalIncomeType,
TriggerStrategicActionType,
TriggerStrategicVarType,
} from './types.js';
import {
dispatchGeneralActionEventHandlers,
type GeneralActionEvent,
type GeneralActionEventContext,
type GeneralActionEventHandlers,
type GeneralActionEventType,
} from './events.js';
export interface GeneralActionModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
interface GeneralActionModuleBase<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
getName?: (() => string) | undefined;
getInfo?: (() => string) | undefined;
getPreTurnExecuteTriggerList?:
| ((context: GeneralActionContext<TriggerState>) => GeneralTriggerCaller<TriggerState> | null)
| undefined;
((context: GeneralActionContext<TriggerState>) => GeneralTriggerCaller<TriggerState> | null) | undefined;
onCalcDomestic?:
| ((
@@ -59,22 +63,46 @@ export interface GeneralActionModule<TriggerState extends GeneralTriggerState =
onCalcNationalIncome?:
| ((context: GeneralActionContext<TriggerState>, type: TriggerNationalIncomeType, amount: number) => number)
| undefined;
onArbitraryAction?:
| ((
context: GeneralActionContext<TriggerState>,
actionType: TriggerActionType,
phase?: TriggerActionPhase | null,
aux?: Record<string, unknown> | null
) => Record<string, unknown> | null)
| undefined;
}
export class GeneralActionPipeline<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly modules: GeneralActionModule<TriggerState>[];
interface GeneralActionLeafModule<TriggerState extends GeneralTriggerState> {
eventHandlers?: GeneralActionEventHandlers<TriggerState> | undefined;
handleEvent?: never;
}
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
this.modules = modules.filter(Boolean) as GeneralActionModule<TriggerState>[];
interface GeneralActionCompositeModule<TriggerState extends GeneralTriggerState> {
eventHandlers?: never;
handleEvent<K extends GeneralActionEventType>(
context: GeneralActionEventContext<K, TriggerState>,
event: GeneralActionEvent<K, TriggerState>
): GeneralActionEvent<K, TriggerState>;
}
/**
* leaf handler와 router는 . module이
* .
*/
export type GeneralActionModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
GeneralActionModuleBase<TriggerState> &
(GeneralActionLeafModule<TriggerState> | GeneralActionCompositeModule<TriggerState>);
export const dispatchGeneralActionModuleEvent = <
TriggerState extends GeneralTriggerState,
K extends GeneralActionEventType,
>(
module: GeneralActionModule<TriggerState>,
context: GeneralActionEventContext<K, TriggerState>,
event: GeneralActionEvent<K, TriggerState>
): GeneralActionEvent<K, TriggerState> =>
module.handleEvent
? module.handleEvent(context, event)
: dispatchGeneralActionEventHandlers(module.eventHandlers, context, event);
export class GeneralActionPipeline<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly modules: ReadonlyArray<GeneralActionModule<TriggerState>>;
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>) {
this.modules = modules.filter(Boolean) as ReadonlyArray<GeneralActionModule<TriggerState>>;
}
getPreTurnExecuteTriggerList(context: GeneralActionContext<TriggerState>): GeneralTriggerCaller<TriggerState> {
@@ -170,21 +198,13 @@ export class GeneralActionPipeline<TriggerState extends GeneralTriggerState = Ge
return current;
}
onArbitraryAction(
context: GeneralActionContext<TriggerState>,
actionType: TriggerActionType,
phase?: TriggerActionPhase | null,
aux?: Record<string, unknown> | null
): Record<string, unknown> | null {
let current = aux ?? null;
dispatch<K extends GeneralActionEventType>(
context: GeneralActionEventContext<K, TriggerState>,
event: GeneralActionEvent<K, TriggerState>
): GeneralActionEvent<K, TriggerState> {
let current = event;
for (const module of this.modules) {
if (!module.onArbitraryAction) {
continue;
}
const result = module.onArbitraryAction(context, actionType, phase, current);
if (result !== undefined) {
current = result;
}
current = dispatchGeneralActionModuleEvent(module, context, current);
}
return current;
}
@@ -0,0 +1,6 @@
export * from './events.js';
export * from './general.js';
export * from './types.js';
export * from './officerLevel.js';
export * from './bundle.js';
export * from './traits/index.js';
@@ -1,6 +1,6 @@
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 { GeneralActionModule } from './general.js';
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
const resolveOfficerLevel = (context: {
@@ -65,9 +65,7 @@ const officerWarModule: WarActionModule = {
},
};
export const createOfficerLevelActionModules = <
TriggerState extends GeneralTriggerState = GeneralTriggerState,
>(): {
export const createOfficerLevelActionModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(): {
general: GeneralActionModule<TriggerState>;
war: WarActionModule<TriggerState>;
} => ({
@@ -1,21 +1,24 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { dispatchGeneralActionModuleEvent, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import {
type GeneralActionEvent,
type GeneralActionEventContext,
type GeneralActionEventType,
} from '@sammo-ts/logic/actionModules/events.js';
import type {
GeneralStatName,
TriggerActionPhase,
TriggerActionType,
TriggerDomesticActionType,
TriggerDomesticVarType,
TriggerNationalIncomeType,
TriggerStrategicActionType,
TriggerStrategicVarType,
WarStatName,
} from '@sammo-ts/logic/triggers/types.js';
} from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { TraitKind, TraitModule, TraitModuleRegistry } from './types.js';
import type { TraitCatalog, TraitKind, TraitModule } from './types.js';
const resolveTraitKey = (
context: {
@@ -43,7 +46,7 @@ const resolveTraitKey = (
};
const resolveModule = <TriggerState extends GeneralTriggerState>(
registry: TraitModuleRegistry<TriggerState>,
catalog: TraitCatalog<TriggerState>,
kind: TraitKind,
key: string | null
): TraitModule<TriggerState> | null => {
@@ -52,29 +55,27 @@ const resolveModule = <TriggerState extends GeneralTriggerState>(
}
let bucket: Map<string, TraitModule<TriggerState>>;
if (kind === 'domestic') {
bucket = registry.domestic;
bucket = catalog.domestic;
} else if (kind === 'war') {
bucket = registry.war;
bucket = catalog.war;
} else if (kind === 'nation') {
bucket = registry.nation;
bucket = catalog.nation;
} else {
bucket = registry.personality;
bucket = catalog.personality;
}
return bucket.get(key) ?? null;
};
// General 파이프라인에서 특성(특기/성격) 모듈을 선택해 위임하는 라우터.
export class TraitGeneralActionRouter<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionModule<TriggerState> {
export class TraitGeneralActionRouter<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
constructor(
private readonly kind: TraitKind,
private readonly registry: TraitModuleRegistry<TriggerState>
private readonly catalog: TraitCatalog<TriggerState>
) {}
private getModule(context: GeneralActionContext<TriggerState>): TraitModule<TriggerState> | null {
const key = resolveTraitKey(context, this.kind);
return resolveModule(this.registry, this.kind, key);
return resolveModule(this.catalog, this.kind, key);
}
getPreTurnExecuteTriggerList(context: GeneralActionContext<TriggerState>) {
@@ -132,15 +133,15 @@ export class TraitGeneralActionRouter<
return module?.onCalcNationalIncome?.(context, type, amount) ?? amount;
}
onArbitraryAction(
context: GeneralActionContext<TriggerState>,
actionType: TriggerActionType,
phase?: TriggerActionPhase | null,
aux?: Record<string, unknown> | null
): Record<string, unknown> | null {
handleEvent<K extends GeneralActionEventType>(
context: GeneralActionEventContext<K, TriggerState>,
event: GeneralActionEvent<K, TriggerState>
): GeneralActionEvent<K, TriggerState> {
const module = this.getModule(context);
const result = module?.onArbitraryAction?.(context, actionType, phase, aux);
return result === undefined ? (aux ?? null) : result;
if (!module) {
return event;
}
return dispatchGeneralActionModuleEvent(module, context, event);
}
}
@@ -150,12 +151,12 @@ export class TraitWarActionRouter<
> implements WarActionModule<TriggerState> {
constructor(
private readonly kind: TraitKind,
private readonly registry: TraitModuleRegistry<TriggerState>
private readonly catalog: TraitCatalog<TriggerState>
) {}
private getModule(context: WarActionContext<TriggerState>): TraitModule<TriggerState> | null {
const key = resolveTraitKey(context, this.kind);
return resolveModule(this.registry, this.kind, key);
return resolveModule(this.catalog, this.kind, key);
}
getBattleInitTriggerList(context: WarActionContext<TriggerState>): WarTriggerCaller | null {
@@ -199,32 +200,46 @@ export class TraitWarActionRouter<
}
export interface TraitModuleSet<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: GeneralActionModule<TriggerState>[];
general: ReadonlyArray<GeneralActionModule<TriggerState>>;
war: WarActionModule<TriggerState>[];
}
export const createTraitModuleRegistry = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(options: {
export const createTraitCatalog = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(options: {
domestic?: TraitModule<TriggerState>[];
war?: TraitModule<TriggerState>[];
personality?: TraitModule<TriggerState>[];
nation?: TraitModule<TriggerState>[];
}): TraitModuleRegistry<TriggerState> => {
}): TraitCatalog<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>>();
const insert = (
bucket: Map<string, TraitModule<TriggerState>>,
kind: TraitKind,
module: TraitModule<TriggerState>
): void => {
if (module.kind !== kind) {
throw new Error(`Trait ${module.key} declared kind ${module.kind}, expected ${kind}`);
}
if (bucket.has(module.key)) {
throw new Error(`Duplicate ${kind} trait key: ${module.key}`);
}
bucket.set(module.key, module);
};
for (const module of options.domestic ?? []) {
domestic.set(module.key, module);
insert(domestic, 'domestic', module);
}
for (const module of options.war ?? []) {
war.set(module.key, module);
insert(war, 'war', module);
}
for (const module of options.personality ?? []) {
personality.set(module.key, module);
insert(personality, 'personality', module);
}
for (const module of options.nation ?? []) {
nation.set(module.key, module);
insert(nation, 'nation', module);
}
return { domestic, war, personality, nation };
@@ -232,18 +247,18 @@ export const createTraitModuleRegistry = <TriggerState extends GeneralTriggerSta
// 특성 레지스트리를 General/전투 파이프라인용 모듈 목록으로 변환한다.
export const createTraitModules = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
registry: TraitModuleRegistry<TriggerState>
catalog: TraitCatalog<TriggerState>
): TraitModuleSet<TriggerState> => ({
general: [
new TraitGeneralActionRouter<TriggerState>('domestic', registry),
new TraitGeneralActionRouter<TriggerState>('war', registry),
new TraitGeneralActionRouter<TriggerState>('personality', registry),
new TraitGeneralActionRouter<TriggerState>('nation', registry),
new TraitGeneralActionRouter<TriggerState>('domestic', catalog),
new TraitGeneralActionRouter<TriggerState>('war', catalog),
new TraitGeneralActionRouter<TriggerState>('personality', catalog),
new TraitGeneralActionRouter<TriggerState>('nation', catalog),
],
war: [
new TraitWarActionRouter<TriggerState>('domestic', registry),
new TraitWarActionRouter<TriggerState>('war', registry),
new TraitWarActionRouter<TriggerState>('personality', registry),
new TraitWarActionRouter<TriggerState>('nation', registry),
new TraitWarActionRouter<TriggerState>('domestic', catalog),
new TraitWarActionRouter<TriggerState>('war', catalog),
new TraitWarActionRouter<TriggerState>('personality', catalog),
new TraitWarActionRouter<TriggerState>('nation', catalog),
],
});
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 경작
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 귀모
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 발명
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 상재
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 수비
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 인덕
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 축성
export const traitModule: TraitModule = {
@@ -1,5 +1,5 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/actionModules/traits/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 내정 특기: 통찰
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
export const DOMESTIC_TRAIT_KEYS = [
'che_인덕',
@@ -1,7 +1,7 @@
export * from './types.js';
export * from './requirements.js';
export * from './selector.js';
export * from './registry.js';
export * from './catalog.js';
export * from './domestic/index.js';
export * from './war/index.js';
export * from './personality/index.js';
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 덕가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 도가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 도적
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 명가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 묵가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 법가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 병가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 불가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 오두미도
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 유가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 음양가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 종횡가
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 중립
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
// 국가 성향: 태평도
export const traitModule: TraitModule = {
@@ -1,4 +1,4 @@
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
export const NATION_TRAIT_KEYS = [
'che_중립',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_대의',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_안전',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_왕좌',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_유지',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_은둔',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_의협',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_재간',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_정복',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_출세',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_패권',
@@ -1,7 +1,7 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/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';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
export const traitModule: TraitModule = {
key: 'che_할거',
@@ -1,4 +1,4 @@
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
export const PERSONALITY_TRAIT_KEYS = [
'che_안전',
@@ -1,5 +1,5 @@
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/actionModules/general.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import type { TraitSelection } from './requirements.js';
@@ -21,7 +21,7 @@ export interface TraitModuleExport<TriggerState extends GeneralTriggerState = Ge
traitModule: TraitModule<TriggerState>;
}
export interface TraitModuleRegistry<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
export interface TraitCatalog<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
domestic: Map<string, TraitModule<TriggerState>>;
war: Map<string, TraitModule<TriggerState>>;
personality: Map<string, TraitModule<TriggerState>>;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_격노발동, che_격노시도 } from '@sammo-ts/logic/war/triggers/che_격노.js';
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_부상무효 } from '@sammo-ts/logic/war/triggers/che_견고.js';
@@ -1,8 +1,8 @@
import { TraitRequirement, TraitWeightType } from '../requirements.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { WarUnitCity } from '@sammo-ts/logic/war/units.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
@@ -1,8 +1,8 @@
import { TraitRequirement, TraitWeightType } from '../requirements.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
@@ -1,8 +1,8 @@
import { TraitRequirement, TraitWeightType } from '../requirements.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
@@ -1,8 +1,8 @@
import { TraitRequirement, TraitWeightType } from '../requirements.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
@@ -1,8 +1,8 @@
import { TraitRequirement, TraitWeightType } from '../requirements.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_돌격지속 } from '@sammo-ts/logic/war/triggers/che_돌격.js';
@@ -1,8 +1,8 @@
import type { TriggerValue } from '@sammo-ts/logic/domain/entities.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
@@ -53,11 +53,7 @@ export const traitModule: TraitModule = {
// Note: unit.getGeneral() is only available for WarUnitGeneral.
// In a real scenario, we should check if unit is WarUnitGeneral.
if (hasGeneral(unit)) {
const killnum = getMetaNumber(
unit.getGeneral().meta as Record<string, TriggerValue>,
'rank_killnum',
0
);
const killnum = getMetaNumber(unit.getGeneral().meta as Record<string, TriggerValue>, 'rank_killnum', 0);
const logVal = Math.log2(Math.max(1, killnum / 5));
attackMultiplier += logVal / 20;
defenceMultiplier -= logVal / 50;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_반계발동, che_반계시도 } from '@sammo-ts/logic/war/triggers/che_반계.js';
@@ -1,8 +1,8 @@
import { TraitRequirement, TraitWeightType } from '../requirements.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/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_위압발동, che_위압시도 } from '@sammo-ts/logic/war/triggers/che_위압.js';
@@ -2,7 +2,7 @@ import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import { triggerModule as cheUisulTriggerModule } from '@sammo-ts/logic/war/triggers/che_의술.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
// 전투 특기: 의술
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_저격발동, che_저격시도 } from '@sammo-ts/logic/war/triggers/che_저격.js';
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
const RECRUIT_TRAIN = 70;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_필살강화_회피불가 } from '@sammo-ts/logic/war/triggers/che_필살.js';
@@ -1,7 +1,7 @@
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
@@ -1,4 +1,4 @@
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/actionModules/traits/types.js';
export const WAR_TRAIT_KEYS = [
'che_귀병',
@@ -1,7 +1,3 @@
export type TriggerActionType = '장비매매' | 'GeneralCommand';
export type TriggerActionPhase = '판매' | '구매';
export type TriggerDomesticActionType =
| '상업'
| '농업'
@@ -22,6 +22,11 @@ export type ActionContextBase = {
worldView?: GeneralWorldView;
rng: ActionRandomSource;
uniqueLottery?: UniqueLotteryRunner;
time?: {
year: number;
month: number;
startYear: number;
};
};
export type ActionResolveContext = ActionContextBase & Record<string, unknown>;
@@ -1,7 +1,8 @@
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import type { NationTraitModule } from '@sammo-ts/logic/triggers/special/nation/index.js';
import type { NationTraitModule } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
import type { RefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js';
export interface TurnCommandItemCatalogEntry {
slot: 'horse' | 'weapon' | 'book' | 'item';
@@ -56,7 +57,7 @@ export interface TurnCommandEnv {
npcSeizureMessageProb?: number;
maxResourceActionAmount: number;
itemCatalog?: Record<string, TurnCommandItemCatalogEntry>;
generalActionModules?: Array<GeneralActionModule>;
warActionModules?: Array<WarActionModule>;
generalActionModules?: RefOrderedActionStack<GeneralActionModule>;
warActionModules?: RefOrderedActionStack<WarActionModule>;
nationTraitModules?: Array<NationTraitModule>;
}
@@ -8,7 +8,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { setMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { DOMESTIC_TRAIT_KEYS } from '@sammo-ts/logic/triggers/special/domestic/index.js';
import { DOMESTIC_TRAIT_KEYS } from '@sammo-ts/logic/actionModules/traits/domestic/index.js';
export interface ResetSpecialDomesticArgs {}
@@ -6,7 +6,7 @@ import {
} from './che_징병.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
@@ -14,7 +14,7 @@ export class ActionDefinition<
public override readonly key = 'che_모병';
public override readonly name = '모병';
constructor(modules: GeneralActionModule<TriggerState>[]) {
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState>>) {
super(modules, {
actionName: '모병',
costOffset: 2,
@@ -1,7 +1,7 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, notWanderingNation, occupiedCity, suppliedCity } from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -31,7 +31,7 @@ export class ActionResolver<
readonly key = ACTION_KEY;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>) {
this.pipeline = new GeneralActionPipeline(modules);
}
@@ -186,7 +186,7 @@ export class ActionDefinition<
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>) {
this.resolver = new ActionResolver(modules);
}
@@ -16,7 +16,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { clamp } from 'es-toolkit';
import { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
export interface BoostMoraleArgs {}
@@ -11,8 +11,8 @@ import {
reqGeneralRice,
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { TriggerDomesticActionType } from '@sammo-ts/logic/triggers/types.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { TriggerDomesticActionType } from '@sammo-ts/logic/actionModules/types.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -168,7 +168,7 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
private readonly config: InvestmentConfig;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: InvestmentEnvironment,
config: InvestmentConfig = DEFAULT_CONFIG
) {
@@ -324,7 +324,7 @@ export class ActionResolver<
private readonly config: InvestmentConfig;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: InvestmentEnvironment,
config: InvestmentConfig = DEFAULT_CONFIG
) {
@@ -398,7 +398,7 @@ export class ActionDefinition<
private readonly config: InvestmentConfig;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: InvestmentEnvironment,
config: InvestmentConfig = DEFAULT_CONFIG
) {
@@ -25,7 +25,7 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
import type { GeneralTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { parseArgsWithSchema } from '../parseArgs.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
import {
buildStrategyActionContext,
@@ -6,7 +6,7 @@ import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-t
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
export interface DisbandArgs {}
@@ -9,7 +9,7 @@ import type {
} from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -175,12 +175,11 @@ const legacyChoiceIndex = (rng: RandomGenerator, length: number): number => {
throw new Error('Empty items');
}
const inclusive = rng as InclusiveRandomGenerator;
return inclusive.nextIntInclusive
? inclusive.nextIntInclusive(length - 1)
: rng.nextInt(0, length);
return inclusive.nextIntInclusive ? inclusive.nextIntInclusive(length - 1) : rng.nextInt(0, length);
};
const legacyChoice = <T>(rng: RandomGenerator, values: readonly T[]): T => values[legacyChoiceIndex(rng, values.length)]!;
const legacyChoice = <T>(rng: RandomGenerator, values: readonly T[]): T =>
values[legacyChoiceIndex(rng, values.length)]!;
const resolveCandidate = (
context: TalentScoutResolveContext,
@@ -239,7 +238,10 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: TalentScoutEnvironment;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: TalentScoutEnvironment
) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
}
@@ -269,7 +271,10 @@ export class ActionResolver<
private readonly env: TalentScoutEnvironment;
private readonly command: CommandResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: TalentScoutEnvironment
) {
this.env = env;
this.command = new CommandResolver(modules, env);
}
@@ -353,20 +358,19 @@ export class ActionResolver<
let dex: [number, number, number, number, number];
if (pickType === '무') {
const distributions = [
[dexTotal * 5 / 8, dexTotal / 8, dexTotal / 8, dexTotal / 8],
[dexTotal / 8, dexTotal * 5 / 8, dexTotal / 8, dexTotal / 8],
[dexTotal / 8, dexTotal / 8, dexTotal * 5 / 8, dexTotal / 8],
[(dexTotal * 5) / 8, dexTotal / 8, dexTotal / 8, dexTotal / 8],
[dexTotal / 8, (dexTotal * 5) / 8, dexTotal / 8, dexTotal / 8],
[dexTotal / 8, dexTotal / 8, (dexTotal * 5) / 8, dexTotal / 8],
] as const;
const picked = legacyChoice(context.rng, distributions);
dex = [picked[0], picked[1], picked[2], picked[3], averageDex[4]];
} else if (pickType === '지') {
dex = [dexTotal / 8, dexTotal / 8, dexTotal / 8, dexTotal * 5 / 8, averageDex[4]];
dex = [dexTotal / 8, dexTotal / 8, dexTotal / 8, (dexTotal * 5) / 8, averageDex[4]];
} else {
dex = [dexTotal / 4, dexTotal / 4, dexTotal / 4, dexTotal / 4, averageDex[4]];
}
const personality =
resolvedCandidate.personality ??
legacyChoice(context.rng, this.env.availablePersonalities ?? ['che_안전']);
resolvedCandidate.personality ?? legacyChoice(context.rng, this.env.availablePersonalities ?? ['che_안전']);
const name = this.env.decorateName
? this.env.decorateName(resolvedCandidate.name, NPC_TYPE)
: `${resolvedCandidate.name}`;
@@ -374,10 +378,7 @@ export class ActionResolver<
const turnSecond = randomRangeInt(context.rng, 0, context.turnTermMinutes * 60 - 1);
const turnFraction = randomRangeInt(context.rng, 0, 999_999);
const killturn =
(deathYear - context.currentYear) * 12 +
randomRangeInt(context.rng, 0, 11) +
context.currentMonth -
1;
(deathYear - context.currentYear) * 12 + randomRangeInt(context.rng, 0, 11) + context.currentMonth - 1;
const meta: GeneralMeta = {
killturn,
npcType: NPC_TYPE,
@@ -461,7 +462,10 @@ export class ActionDefinition<
private readonly command: CommandResolver<TriggerState>;
private readonly resolver: ActionResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: TalentScoutEnvironment
) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
@@ -517,6 +521,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '인사',
reqArg: false,
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? [], env),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
};
@@ -8,8 +8,12 @@ import {
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import type {
GeneralActionEffect,
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
import { z } from 'zod';
@@ -19,6 +23,8 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
import { parseArgsWithSchema } from '../parseArgs.js';
import { equipNewItem, removeEquippedItem } from '@sammo-ts/logic/items/inventory.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { createGeneralActionEvent } from '@sammo-ts/logic/actionModules/events.js';
const ACTION_NAME = '장비매매';
const ACTION_KEY = 'che_장비매매';
@@ -54,8 +60,11 @@ export class ActionDefinition<
> implements GeneralActionDefinition<TriggerState, TradeItemArgs> {
public readonly key = ACTION_KEY;
public readonly name = ACTION_NAME;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(private readonly env: TurnCommandEnv) {}
constructor(private readonly env: TurnCommandEnv) {
this.pipeline = new GeneralActionPipeline(env.generalActionModules ?? []);
}
parseArgs(raw: unknown): TradeItemArgs | null {
const args = parseArgsWithSchema(ARGS_SCHEMA, raw);
@@ -144,6 +153,7 @@ export class ActionDefinition<
): GeneralActionOutcome<TriggerState> {
const general = context.general;
const nation = context.nation;
const nationResourcesBeforeEvent = nation ? { gold: nation.gold, rice: nation.rice } : null;
const itemType = args.itemType;
const requestedItemCode = args.itemCode;
@@ -162,14 +172,6 @@ export class ActionDefinition<
const josaUl = JosaUtil.pick(itemRawName, '을');
const nextGold = buying ? Math.max(0, general.gold - itemCost) : general.gold + Math.floor(itemCost / 2);
if (buying) {
equipNewItem(general, itemType, finalItemCode, {
...(item?.initialCharges === undefined ? {} : { charges: item.initialCharges }),
});
} else {
removeEquippedItem(general, itemType);
}
if (buying) {
context.addLog(`<C>${itemName}</>${josaUl} 구입했습니다.`, {
scope: LogScope.GENERAL,
@@ -184,6 +186,43 @@ export class ActionDefinition<
});
}
general.gold = nextGold;
if (buying) {
equipNewItem(general, itemType, finalItemCode, {
...(item?.initialCharges === undefined ? {} : { charges: item.initialCharges }),
});
}
const eventLog = {
push: (message: string) =>
context.addLog(message, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
};
if (buying) {
this.pipeline.dispatch(
{ ...context, log: eventLog },
createGeneralActionEvent<TriggerState, 'item.purchased'>('item.purchased', {
itemKey: finalItemCode,
slot: itemType,
})
);
} else {
if (!context.time) {
throw new Error('장비 판매 이벤트에는 현재 연도와 시나리오 시작 연도가 필요합니다.');
}
this.pipeline.dispatch(
{ ...context, time: context.time, log: eventLog },
createGeneralActionEvent<TriggerState, 'item.sold'>('item.sold', {
itemKey: finalItemCode,
slot: itemType,
})
);
removeEquippedItem(general, itemType);
}
if (!buying && item && !item.buyable && nation) {
const josaYi = JosaUtil.pick(general.name, '이');
context.addLog(`<Y>${general.name}</>${josaYi} <C>${itemName}</>${josaUl} 판매했습니다!`, {
@@ -201,13 +240,31 @@ export class ActionDefinition<
tryApplyUniqueLottery(context, { acquireType: '아이템', reason: ACTION_NAME });
const effects: Array<GeneralActionEffect<TriggerState>> = [
createGeneralPatchEffect<TriggerState>({
gold: general.gold,
rice: general.rice,
experience: general.experience + 10,
}),
];
if (
nation &&
nationResourcesBeforeEvent &&
(nation.gold !== nationResourcesBeforeEvent.gold || nation.rice !== nationResourcesBeforeEvent.rice)
) {
effects.push(
createNationPatchEffect(
{
gold: nation.gold,
rice: nation.rice,
},
nation.id
)
);
}
return {
effects: [
createGeneralPatchEffect<TriggerState>({
gold: nextGold,
experience: general.experience + 10,
}),
],
effects,
};
}
}
@@ -8,7 +8,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { setMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { WAR_TRAIT_KEYS } from '@sammo-ts/logic/triggers/special/war/index.js';
import { WAR_TRAIT_KEYS } from '@sammo-ts/logic/actionModules/traits/war/index.js';
export interface ResetSpecialWarArgs {}
@@ -9,7 +9,7 @@ import {
reqGeneralGold,
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -217,7 +217,7 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: RecruitEnvironment;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
}
@@ -331,7 +331,7 @@ export class ActionResolver<
private readonly env: RecruitEnvironment;
private readonly command: CommandResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
this.env = env;
this.command = new CommandResolver(modules, env);
}
@@ -444,7 +444,7 @@ export class ActionDefinition<
private readonly resolver: ActionResolver<TriggerState>;
private readonly env: RecruitEnvironment;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
constructor(modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
this.env = env;
@@ -31,7 +31,8 @@ import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo
import { resolveWarAftermath } from '@sammo-ts/logic/war/aftermath.js';
import { resolveWarBattle } from '@sammo-ts/logic/war/engine.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import type { NationTraitModule } from '@sammo-ts/logic/triggers/special/nation/index.js';
import type { NationTraitModule } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import { increaseMetaNumber, simpleSerialize } from '@sammo-ts/logic/war/utils.js';
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -163,9 +164,7 @@ const pickCandidateCity = (
const legacyCompatibleRng = rng as typeof rng & {
nextIntInclusive?: (maxInclusive: number) => number;
};
const index =
legacyCompatibleRng.nextIntInclusive?.(items.length - 1) ??
rng.nextInt(0, items.length);
const index = legacyCompatibleRng.nextIntInclusive?.(items.length - 1) ?? rng.nextInt(0, items.length);
return items[index]!;
};
const distances = Array.from(distanceList.keys()).sort((a, b) => a - b);
@@ -255,15 +254,18 @@ export class ActionDefinition<
getInheritanceActiveActionAmount(): number {
return 1;
}
private readonly warModules: Array<WarActionModule<TriggerState>>;
private readonly warModules: ReadonlyArray<WarActionModule<TriggerState>>;
private readonly nationTraitModules: Map<string, NationTraitModule>;
private readonly generalModules: ReadonlyArray<GeneralActionModule<TriggerState>>;
constructor(
modules: Array<WarActionModule<TriggerState> | null | undefined> = [],
nationTraitModules: NationTraitModule[] = []
modules: ReadonlyArray<WarActionModule<TriggerState> | null | undefined> = [],
nationTraitModules: NationTraitModule[] = [],
generalModules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined> = []
) {
this.warModules = modules.filter(Boolean) as Array<WarActionModule<TriggerState>>;
this.warModules = modules.filter(Boolean) as ReadonlyArray<WarActionModule<TriggerState>>;
this.nationTraitModules = new Map(nationTraitModules.map((module) => [module.key, module]));
this.generalModules = generalModules.filter(Boolean) as ReadonlyArray<GeneralActionModule<TriggerState>>;
}
parseArgs(raw: unknown): DispatchArgs | null {
@@ -474,15 +476,12 @@ export class ActionDefinition<
config: context.aftermathConfig,
time,
hiddenSeed: context.seedBase,
generalActionModules: this.generalModules,
calcNationTechGain: ({ nation, baseGain }) => {
const module = this.nationTraitModules.get(nation.typeCode);
return (
module?.onCalcDomestic?.(
{ general: context.general, nation },
'기술',
'score',
baseGain
) ?? baseGain
module?.onCalcDomestic?.({ general: context.general, nation }, '기술', 'score', baseGain) ??
baseGain
);
},
});
@@ -601,5 +600,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
availabilityArgs: { destCityId: 0 },
argsSchema: ARGS_SCHEMA,
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.warActionModules ?? [], env.nationTraitModules ?? []),
new ActionDefinition(env.warActionModules ?? [], env.nationTraitModules ?? [], env.generalActionModules ?? []),
};
@@ -25,7 +25,7 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
import type { GeneralTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { parseArgsWithSchema } from '../parseArgs.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
import {
buildStrategyActionContext,
@@ -25,7 +25,7 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
import type { GeneralTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { parseArgsWithSchema } from '../parseArgs.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
import {
buildStrategyActionContext,
@@ -12,7 +12,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -125,7 +125,10 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
private readonly env: FireAttackEnvironment;
private readonly statKey: 'leadership' | 'strength' | 'intelligence';
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: FireAttackEnvironment
) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
this.statKey = env.statKey ?? 'intelligence';
@@ -287,7 +290,10 @@ export class ActionResolver<
private readonly command: CommandResolver<TriggerState>;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: FireAttackEnvironment
) {
this.command = new CommandResolver(modules, env);
this.pipeline = new GeneralActionPipeline(modules);
}
@@ -376,8 +382,7 @@ export class ActionResolver<
);
const itemCode = general.role.items.item;
const actionResult = consumeSuccessfulStrategyItem(this.pipeline, context);
const consumedItems = Array.isArray(actionResult?.['consumedItems']) ? actionResult['consumedItems'] : [];
const consumedItems = consumeSuccessfulStrategyItem(this.pipeline, context);
if (typeof itemCode === 'string' && consumedItems.includes(itemCode)) {
context.addLog(`<C>${itemCode}</>${JosaUtil.pick(itemCode, '을')} 사용!`, {
format: LogFormat.PLAIN,
@@ -405,7 +410,10 @@ export class ActionDefinition<
private readonly command: CommandResolver<TriggerState>;
private readonly resolver: ActionResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
env: FireAttackEnvironment
) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
@@ -15,7 +15,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
const ACTION_NAME = '맹훈련';
const ACTION_KEY = 'cr_맹훈련';
@@ -1,4 +1,4 @@
import { NATION_TRAIT_KEYS } from '@sammo-ts/logic/triggers/special/nation/index.js';
import { NATION_TRAIT_KEYS } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
import { getLegacyStringWidth } from '@sammo-ts/logic/troop/management.js';
import { z } from 'zod';
@@ -1,12 +1,15 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { createGeneralActionEvent } from '@sammo-ts/logic/actionModules/events.js';
export const consumeSuccessfulStrategyItem = <TriggerState extends GeneralTriggerState>(
pipeline: GeneralActionPipeline<TriggerState>,
context: GeneralActionResolveContext<TriggerState>
): Record<string, unknown> | null =>
pipeline.onArbitraryAction(context, 'GeneralCommand', null, {
command: '계략',
success: true,
});
): readonly string[] =>
pipeline.dispatch(
context,
createGeneralActionEvent<TriggerState, 'strategy.succeeded'>('strategy.succeeded', {
consumedItems: [],
})
).payload.consumedItems;
@@ -7,7 +7,7 @@ import {
existsDestNation,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -51,7 +51,7 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
private readonly initialNationGenLimit = 10
) {
this.pipeline = new GeneralActionPipeline(modules);
@@ -75,7 +75,10 @@ export class ActionResolver<
readonly key = 'che_급습';
private readonly command: CommandResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
initialNationGenLimit = 10
) {
this.command = new CommandResolver(modules, initialNationGenLimit);
}
@@ -177,7 +180,10 @@ export class ActionDefinition<
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
initialNationGenLimit = 10
) {
this.resolver = new ActionResolver(modules, initialNationGenLimit);
}
@@ -6,7 +6,7 @@ import {
occupiedCity,
occupiedDestCity,
} from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -46,7 +46,7 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
private readonly initialNationGenLimit = 10
) {
this.pipeline = new GeneralActionPipeline(modules);
@@ -70,7 +70,10 @@ export class ActionResolver<
readonly key = 'che_백성동원';
private readonly command: CommandResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
initialNationGenLimit = 10
) {
this.command = new CommandResolver(modules, initialNationGenLimit);
}
@@ -154,7 +157,10 @@ export class ActionDefinition<
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, initialNationGenLimit = 10) {
constructor(
modules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>,
initialNationGenLimit = 10
) {
this.resolver = new ActionResolver(modules, initialNationGenLimit);
}

Some files were not shown because too many files have changed in this diff Show More