diff --git a/AGENTS.md b/AGENTS.md index b43ddb8..a90a690 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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로 연결해 주세요. diff --git a/README.md b/README.md index 3d8b47c..ad3caa5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/game-api/src/battleSim/processor.ts b/app/game-api/src/battleSim/processor.ts index 3477d0e..d6f99e3 100644 --- a/app/game-api/src/battleSim/processor.ts +++ b/app/game-api/src/battleSim/processor.ts @@ -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 => { const crewTypeCatalog = compileCrewTypeCatalog(unitSet, crewTypeWarTriggerRegistry); - return [...traitWarModules, crewTypeCatalog.warActionModule, inheritBuffModules.war, ...itemWarModules]; + return createRefOrderedActionStack({ + 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); diff --git a/app/game-engine/src/turn/monthlyDisasterAction.ts b/app/game-engine/src/turn/monthlyDisasterAction.ts index b9bfd15..08b5b31 100644 --- a/app/game-engine/src/turn/monthlyDisasterAction.ts +++ b/app/game-engine/src/turn/monthlyDisasterAction.ts @@ -22,7 +22,11 @@ const DISASTER_TEXT_BY_MONTH: Readonly> = { 1: [ { title: '【재난】', stateCode: 4, body: '역병이 발생하여 도시가 황폐해지고 있습니다.' }, { title: '【재난】', stateCode: 5, body: '지진으로 피해가 속출하고 있습니다.' }, - { title: '【재난】', stateCode: 3, body: '추위가 풀리지 않아 얼어죽는 백성들이 늘어나고 있습니다.' }, + { + title: '【재난】', + stateCode: 3, + body: '추위가 풀리지 않아 얼어죽는 백성들이 늘어나고 있습니다.', + }, { title: '【재난】', 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; }): 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, }); diff --git a/app/game-engine/src/turn/reservedTurnCommands.ts b/app/game-engine/src/turn/reservedTurnCommands.ts index 142eec8..7602116 100644 --- a/app/game-engine/src/turn/reservedTurnCommands.ts +++ b/app/game-engine/src/turn/reservedTurnCommands.ts @@ -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); diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index 3514d32..73150e5 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -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; } diff --git a/docs/architecture/action-module-protocol.md b/docs/architecture/action-module-protocol.md new file mode 100644 index 0000000..cdf097d --- /dev/null +++ b/docs/architecture/action-module-protocol.md @@ -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`가 이벤트별 필수 능력을 정합니다. 예를 들어 + 판매는 RNG와 연월, 도시 점령은 RNG가 없으면 compile되지 않습니다. +- leaf module은 `eventHandlers`에 처리하는 이벤트 key만 선언합니다. +- trait, 병종과 item catalog 같은 합성 router만 내부 `handleEvent`를 + 구현합니다. 두 capability는 `never`를 사용한 상호 배타적 union이라 한 + module에서 동시에 선언할 수 없습니다. +- 새 문자열 action name, `phase`, `aux: Record`를 범용 + 우회로로 추가하지 않습니다. + +새 이벤트를 추가할 때는 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가 +담당합니다. diff --git a/docs/architecture/legacy-engine-triggers.md b/docs/architecture/legacy-engine-triggers.md index 5f6390c..c240b84 100644 --- a/docs/architecture/legacy-engine-triggers.md +++ b/docs/architecture/legacy-engine-triggers.md @@ -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` diff --git a/docs/developer/index.md b/docs/developer/index.md index 160e776..f97dd5c 100644 --- a/docs/developer/index.md +++ b/docs/developer/index.md @@ -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 | ## 읽을 때 지켜야 할 경계 diff --git a/packages/logic/src/actionModules/bundle.ts b/packages/logic/src/actionModules/bundle.ts new file mode 100644 index 0000000..9d8a91a --- /dev/null +++ b/packages/logic/src/actionModules/bundle.ts @@ -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 { + general: RefOrderedActionStack>; + war: RefOrderedActionStack>; + itemModules: ItemModule[]; + nationTraitModules: NationTraitModule[]; +} + +const refActionOrderBrand: unique symbol = Symbol('RefOrderedActionStack'); + +export type RefOrderedActionStack = ReadonlyArray & { + readonly [refActionOrderBrand]: true; +}; + +interface RefActionSlots { + nation: Module; + officer: Module; + domestic: Module; + war: Module; + personality: Module; + crewType: Module | null; + inheritance: Module; + scenario: Module | null; + items: readonly Module[]; +} + +const markRefOrderedActionStack: ( + 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 = (slots: RefActionSlots): RefOrderedActionStack => { + 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 ( + unitSet?: UnitSetDefinition +): Promise> => { + 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[]>, + ]); + const traitCatalog = createTraitCatalog({ domestic, war, personality, nation }); + const officer = createOfficerLevelActionModules(); + const items = createItemActionModules(createItemModuleRegistry(itemModules)); + const inherit = createInheritBuffModules(); + const crewTypeCatalog = unitSet?.crewTypes?.length + ? compileCrewTypeCatalog(unitSet, createCrewTypeWarTriggerRegistry()) + : null; + + return { + general: createRefOrderedActionStack>({ + 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) + : null, + inheritance: inherit.general as GeneralActionModule, + // scenarioEffect는 현재 core runtime module이 없어 명시적으로 빈 slot입니다. + scenario: null, + items: items.general, + }), + war: createRefOrderedActionStack>({ + 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) : null, + inheritance: inherit.war as WarActionModule, + // ref의 scenarioEffect 위치를 보존하되 미이식 module은 별도 gap으로 남깁니다. + scenario: null, + items: items.war, + }), + itemModules, + nationTraitModules: nation, + }; +}; diff --git a/packages/logic/src/actionModules/events.ts b/packages/logic/src/actionModules/events.ts new file mode 100644 index 0000000..36ef653 --- /dev/null +++ b/packages/logic/src/actionModules/events.ts @@ -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 & { rng: RandomGenerator } & (K extends 'item.sold' + ? { time: NonNullable['time']> } + : object) + : GeneralActionContext; + +/** + * 레거시 문자열/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; + [generalActionEventBrand]: K; +}>; + +export type GeneralActionEventHandler = { + bivarianceHack( + context: GeneralActionEventContext, + event: GeneralActionEvent + ): GeneralActionEvent | void; +}['bivarianceHack']; + +export type GeneralActionEventHandlers = Partial<{ + [K in GeneralActionEventType]: GeneralActionEventHandler; +}>; + +export const createGeneralActionEvent = < + TriggerState extends GeneralTriggerState = GeneralTriggerState, + K extends GeneralActionEventType = GeneralActionEventType, +>( + type: K, + payload: GeneralActionEventPayloadMap[K] +): GeneralActionEvent => ({ + type, + payload, + [generalActionEventBrand]: type, +}); + +export const dispatchGeneralActionEventHandlers = < + TriggerState extends GeneralTriggerState, + K extends GeneralActionEventType, +>( + handlers: GeneralActionEventHandlers | null | undefined, + context: GeneralActionEventContext, + event: GeneralActionEvent +): GeneralActionEvent => { + // 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 | undefined; + return handler?.(context, event) ?? event; +}; diff --git a/packages/logic/src/triggers/general-action.ts b/packages/logic/src/actionModules/general.ts similarity index 65% rename from packages/logic/src/triggers/general-action.ts rename to packages/logic/src/actionModules/general.ts index a77bb00..36c80b4 100644 --- a/packages/logic/src/triggers/general-action.ts +++ b/packages/logic/src/actionModules/general.ts @@ -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 { +interface GeneralActionModuleBase { getName?: (() => string) | undefined; getInfo?: (() => string) | undefined; getPreTurnExecuteTriggerList?: - | ((context: GeneralActionContext) => GeneralTriggerCaller | null) - | undefined; + ((context: GeneralActionContext) => GeneralTriggerCaller | null) | undefined; onCalcDomestic?: | (( @@ -59,22 +63,46 @@ export interface GeneralActionModule, type: TriggerNationalIncomeType, amount: number) => number) | undefined; - - onArbitraryAction?: - | (( - context: GeneralActionContext, - actionType: TriggerActionType, - phase?: TriggerActionPhase | null, - aux?: Record | null - ) => Record | null) - | undefined; } -export class GeneralActionPipeline { - private readonly modules: GeneralActionModule[]; +interface GeneralActionLeafModule { + eventHandlers?: GeneralActionEventHandlers | undefined; + handleEvent?: never; +} - constructor(modules: Array | null | undefined>) { - this.modules = modules.filter(Boolean) as GeneralActionModule[]; +interface GeneralActionCompositeModule { + eventHandlers?: never; + handleEvent( + context: GeneralActionEventContext, + event: GeneralActionEvent + ): GeneralActionEvent; +} + +/** + * leaf handler와 합성 router는 상호 배타적입니다. 한 module이 같은 이벤트를 + * 두 경로로 중복 처리할 수 없습니다. + */ +export type GeneralActionModule = + GeneralActionModuleBase & + (GeneralActionLeafModule | GeneralActionCompositeModule); + +export const dispatchGeneralActionModuleEvent = < + TriggerState extends GeneralTriggerState, + K extends GeneralActionEventType, +>( + module: GeneralActionModule, + context: GeneralActionEventContext, + event: GeneralActionEvent +): GeneralActionEvent => + module.handleEvent + ? module.handleEvent(context, event) + : dispatchGeneralActionEventHandlers(module.eventHandlers, context, event); + +export class GeneralActionPipeline { + private readonly modules: ReadonlyArray>; + + constructor(modules: ReadonlyArray | null | undefined>) { + this.modules = modules.filter(Boolean) as ReadonlyArray>; } getPreTurnExecuteTriggerList(context: GeneralActionContext): GeneralTriggerCaller { @@ -170,21 +198,13 @@ export class GeneralActionPipeline, - actionType: TriggerActionType, - phase?: TriggerActionPhase | null, - aux?: Record | null - ): Record | null { - let current = aux ?? null; + dispatch( + context: GeneralActionEventContext, + event: GeneralActionEvent + ): GeneralActionEvent { + 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; } diff --git a/packages/logic/src/actionModules/index.ts b/packages/logic/src/actionModules/index.ts new file mode 100644 index 0000000..c726a7b --- /dev/null +++ b/packages/logic/src/actionModules/index.ts @@ -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'; diff --git a/packages/logic/src/triggers/officerLevel.ts b/packages/logic/src/actionModules/officerLevel.ts similarity index 93% rename from packages/logic/src/triggers/officerLevel.ts rename to packages/logic/src/actionModules/officerLevel.ts index 8aa495b..96a90db 100644 --- a/packages/logic/src/triggers/officerLevel.ts +++ b/packages/logic/src/actionModules/officerLevel.ts @@ -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 = (): { general: GeneralActionModule; war: WarActionModule; } => ({ diff --git a/packages/logic/src/triggers/special/registry.ts b/packages/logic/src/actionModules/traits/catalog.ts similarity index 72% rename from packages/logic/src/triggers/special/registry.ts rename to packages/logic/src/actionModules/traits/catalog.ts index 49924f1..ece7371 100644 --- a/packages/logic/src/triggers/special/registry.ts +++ b/packages/logic/src/actionModules/traits/catalog.ts @@ -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 = ( - registry: TraitModuleRegistry, + catalog: TraitCatalog, kind: TraitKind, key: string | null ): TraitModule | null => { @@ -52,29 +55,27 @@ const resolveModule = ( } let bucket: Map>; 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 { +export class TraitGeneralActionRouter { constructor( private readonly kind: TraitKind, - private readonly registry: TraitModuleRegistry + private readonly catalog: TraitCatalog ) {} private getModule(context: GeneralActionContext): TraitModule | null { const key = resolveTraitKey(context, this.kind); - return resolveModule(this.registry, this.kind, key); + return resolveModule(this.catalog, this.kind, key); } getPreTurnExecuteTriggerList(context: GeneralActionContext) { @@ -132,15 +133,15 @@ export class TraitGeneralActionRouter< return module?.onCalcNationalIncome?.(context, type, amount) ?? amount; } - onArbitraryAction( - context: GeneralActionContext, - actionType: TriggerActionType, - phase?: TriggerActionPhase | null, - aux?: Record | null - ): Record | null { + handleEvent( + context: GeneralActionEventContext, + event: GeneralActionEvent + ): GeneralActionEvent { 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 { constructor( private readonly kind: TraitKind, - private readonly registry: TraitModuleRegistry + private readonly catalog: TraitCatalog ) {} private getModule(context: WarActionContext): TraitModule | null { const key = resolveTraitKey(context, this.kind); - return resolveModule(this.registry, this.kind, key); + return resolveModule(this.catalog, this.kind, key); } getBattleInitTriggerList(context: WarActionContext): WarTriggerCaller | null { @@ -199,32 +200,46 @@ export class TraitWarActionRouter< } export interface TraitModuleSet { - general: GeneralActionModule[]; + general: ReadonlyArray>; war: WarActionModule[]; } -export const createTraitModuleRegistry = (options: { +export const createTraitCatalog = (options: { domestic?: TraitModule[]; war?: TraitModule[]; personality?: TraitModule[]; nation?: TraitModule[]; -}): TraitModuleRegistry => { +}): TraitCatalog => { const domestic = new Map>(); const war = new Map>(); const personality = new Map>(); const nation = new Map>(); + const insert = ( + bucket: Map>, + kind: TraitKind, + module: TraitModule + ): 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 = ( - registry: TraitModuleRegistry + catalog: TraitCatalog ): TraitModuleSet => ({ general: [ - new TraitGeneralActionRouter('domestic', registry), - new TraitGeneralActionRouter('war', registry), - new TraitGeneralActionRouter('personality', registry), - new TraitGeneralActionRouter('nation', registry), + new TraitGeneralActionRouter('domestic', catalog), + new TraitGeneralActionRouter('war', catalog), + new TraitGeneralActionRouter('personality', catalog), + new TraitGeneralActionRouter('nation', catalog), ], war: [ - new TraitWarActionRouter('domestic', registry), - new TraitWarActionRouter('war', registry), - new TraitWarActionRouter('personality', registry), - new TraitWarActionRouter('nation', registry), + new TraitWarActionRouter('domestic', catalog), + new TraitWarActionRouter('war', catalog), + new TraitWarActionRouter('personality', catalog), + new TraitWarActionRouter('nation', catalog), ], }); diff --git a/packages/logic/src/triggers/special/domestic/che_경작.ts b/packages/logic/src/actionModules/traits/domestic/che_경작.ts similarity index 88% rename from packages/logic/src/triggers/special/domestic/che_경작.ts rename to packages/logic/src/actionModules/traits/domestic/che_경작.ts index 870d231..2c20cea 100644 --- a/packages/logic/src/triggers/special/domestic/che_경작.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_경작.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_귀모.ts b/packages/logic/src/actionModules/traits/domestic/che_귀모.ts similarity index 86% rename from packages/logic/src/triggers/special/domestic/che_귀모.ts rename to packages/logic/src/actionModules/traits/domestic/che_귀모.ts index 74544b7..cb89c75 100644 --- a/packages/logic/src/triggers/special/domestic/che_귀모.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_귀모.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_발명.ts b/packages/logic/src/actionModules/traits/domestic/che_발명.ts similarity index 88% rename from packages/logic/src/triggers/special/domestic/che_발명.ts rename to packages/logic/src/actionModules/traits/domestic/che_발명.ts index dea3eac..5935c54 100644 --- a/packages/logic/src/triggers/special/domestic/che_발명.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_발명.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_상재.ts b/packages/logic/src/actionModules/traits/domestic/che_상재.ts similarity index 88% rename from packages/logic/src/triggers/special/domestic/che_상재.ts rename to packages/logic/src/actionModules/traits/domestic/che_상재.ts index d32c852..b16d776 100644 --- a/packages/logic/src/triggers/special/domestic/che_상재.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_상재.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_수비.ts b/packages/logic/src/actionModules/traits/domestic/che_수비.ts similarity index 88% rename from packages/logic/src/triggers/special/domestic/che_수비.ts rename to packages/logic/src/actionModules/traits/domestic/che_수비.ts index 7ae77b1..e24ff58 100644 --- a/packages/logic/src/triggers/special/domestic/che_수비.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_수비.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_인덕.ts b/packages/logic/src/actionModules/traits/domestic/che_인덕.ts similarity index 89% rename from packages/logic/src/triggers/special/domestic/che_인덕.ts rename to packages/logic/src/actionModules/traits/domestic/che_인덕.ts index 1148be5..e071a7e 100644 --- a/packages/logic/src/triggers/special/domestic/che_인덕.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_인덕.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_축성.ts b/packages/logic/src/actionModules/traits/domestic/che_축성.ts similarity index 88% rename from packages/logic/src/triggers/special/domestic/che_축성.ts rename to packages/logic/src/actionModules/traits/domestic/che_축성.ts index 7d2ff70..e9d6bd3 100644 --- a/packages/logic/src/triggers/special/domestic/che_축성.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_축성.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/che_통찰.ts b/packages/logic/src/actionModules/traits/domestic/che_통찰.ts similarity index 88% rename from packages/logic/src/triggers/special/domestic/che_통찰.ts rename to packages/logic/src/actionModules/traits/domestic/che_통찰.ts index c89eae4..ecd1903 100644 --- a/packages/logic/src/triggers/special/domestic/che_통찰.ts +++ b/packages/logic/src/actionModules/traits/domestic/che_통찰.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/domestic/index.ts b/packages/logic/src/actionModules/traits/domestic/index.ts similarity index 98% rename from packages/logic/src/triggers/special/domestic/index.ts rename to packages/logic/src/actionModules/traits/domestic/index.ts index 941bae9..9b74a04 100644 --- a/packages/logic/src/triggers/special/domestic/index.ts +++ b/packages/logic/src/actionModules/traits/domestic/index.ts @@ -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_인덕', diff --git a/packages/logic/src/triggers/special/index.ts b/packages/logic/src/actionModules/traits/index.ts similarity index 88% rename from packages/logic/src/triggers/special/index.ts rename to packages/logic/src/actionModules/traits/index.ts index 1f3f54f..267be1d 100644 --- a/packages/logic/src/triggers/special/index.ts +++ b/packages/logic/src/actionModules/traits/index.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/nation/che_덕가.ts b/packages/logic/src/actionModules/traits/nation/che_덕가.ts similarity index 92% rename from packages/logic/src/triggers/special/nation/che_덕가.ts rename to packages/logic/src/actionModules/traits/nation/che_덕가.ts index c82fc47..9f56f1e 100644 --- a/packages/logic/src/triggers/special/nation/che_덕가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_덕가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_도가.ts b/packages/logic/src/actionModules/traits/nation/che_도가.ts similarity index 89% rename from packages/logic/src/triggers/special/nation/che_도가.ts rename to packages/logic/src/actionModules/traits/nation/che_도가.ts index 6062674..05e1019 100644 --- a/packages/logic/src/triggers/special/nation/che_도가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_도가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_도적.ts b/packages/logic/src/actionModules/traits/nation/che_도적.ts similarity index 90% rename from packages/logic/src/triggers/special/nation/che_도적.ts rename to packages/logic/src/actionModules/traits/nation/che_도적.ts index a361fd6..7a8c847 100644 --- a/packages/logic/src/triggers/special/nation/che_도적.ts +++ b/packages/logic/src/actionModules/traits/nation/che_도적.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_명가.ts b/packages/logic/src/actionModules/traits/nation/che_명가.ts similarity index 91% rename from packages/logic/src/triggers/special/nation/che_명가.ts rename to packages/logic/src/actionModules/traits/nation/che_명가.ts index 0638ef2..0128e6d 100644 --- a/packages/logic/src/triggers/special/nation/che_명가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_명가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_묵가.ts b/packages/logic/src/actionModules/traits/nation/che_묵가.ts similarity index 89% rename from packages/logic/src/triggers/special/nation/che_묵가.ts rename to packages/logic/src/actionModules/traits/nation/che_묵가.ts index 9575874..dbcce98 100644 --- a/packages/logic/src/triggers/special/nation/che_묵가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_묵가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_법가.ts b/packages/logic/src/actionModules/traits/nation/che_법가.ts similarity index 91% rename from packages/logic/src/triggers/special/nation/che_법가.ts rename to packages/logic/src/actionModules/traits/nation/che_법가.ts index 62a6e80..0d1f804 100644 --- a/packages/logic/src/triggers/special/nation/che_법가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_법가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_병가.ts b/packages/logic/src/actionModules/traits/nation/che_병가.ts similarity index 91% rename from packages/logic/src/triggers/special/nation/che_병가.ts rename to packages/logic/src/actionModules/traits/nation/che_병가.ts index fb7537d..7338891 100644 --- a/packages/logic/src/triggers/special/nation/che_병가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_병가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_불가.ts b/packages/logic/src/actionModules/traits/nation/che_불가.ts similarity index 89% rename from packages/logic/src/triggers/special/nation/che_불가.ts rename to packages/logic/src/actionModules/traits/nation/che_불가.ts index 59b64ea..c948fb0 100644 --- a/packages/logic/src/triggers/special/nation/che_불가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_불가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_오두미도.ts b/packages/logic/src/actionModules/traits/nation/che_오두미도.ts similarity index 92% rename from packages/logic/src/triggers/special/nation/che_오두미도.ts rename to packages/logic/src/actionModules/traits/nation/che_오두미도.ts index 04b6d12..cbc3c1f 100644 --- a/packages/logic/src/triggers/special/nation/che_오두미도.ts +++ b/packages/logic/src/actionModules/traits/nation/che_오두미도.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_유가.ts b/packages/logic/src/actionModules/traits/nation/che_유가.ts similarity index 89% rename from packages/logic/src/triggers/special/nation/che_유가.ts rename to packages/logic/src/actionModules/traits/nation/che_유가.ts index dd91eff..52bbdfe 100644 --- a/packages/logic/src/triggers/special/nation/che_유가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_유가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_음양가.ts b/packages/logic/src/actionModules/traits/nation/che_음양가.ts similarity index 92% rename from packages/logic/src/triggers/special/nation/che_음양가.ts rename to packages/logic/src/actionModules/traits/nation/che_음양가.ts index 9849dca..833cc93 100644 --- a/packages/logic/src/triggers/special/nation/che_음양가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_음양가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_종횡가.ts b/packages/logic/src/actionModules/traits/nation/che_종횡가.ts similarity index 93% rename from packages/logic/src/triggers/special/nation/che_종횡가.ts rename to packages/logic/src/actionModules/traits/nation/che_종횡가.ts index a63f81f..35652ec 100644 --- a/packages/logic/src/triggers/special/nation/che_종횡가.ts +++ b/packages/logic/src/actionModules/traits/nation/che_종횡가.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_중립.ts b/packages/logic/src/actionModules/traits/nation/che_중립.ts similarity index 70% rename from packages/logic/src/triggers/special/nation/che_중립.ts rename to packages/logic/src/actionModules/traits/nation/che_중립.ts index 0f708fc..3594971 100644 --- a/packages/logic/src/triggers/special/nation/che_중립.ts +++ b/packages/logic/src/actionModules/traits/nation/che_중립.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/che_태평도.ts b/packages/logic/src/actionModules/traits/nation/che_태평도.ts similarity index 91% rename from packages/logic/src/triggers/special/nation/che_태평도.ts rename to packages/logic/src/actionModules/traits/nation/che_태평도.ts index a7cdb2e..ede3c17 100644 --- a/packages/logic/src/triggers/special/nation/che_태평도.ts +++ b/packages/logic/src/actionModules/traits/nation/che_태평도.ts @@ -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 = { diff --git a/packages/logic/src/triggers/special/nation/index.ts b/packages/logic/src/actionModules/traits/nation/index.ts similarity index 98% rename from packages/logic/src/triggers/special/nation/index.ts rename to packages/logic/src/actionModules/traits/nation/index.ts index d8c7363..acd856d 100644 --- a/packages/logic/src/triggers/special/nation/index.ts +++ b/packages/logic/src/actionModules/traits/nation/index.ts @@ -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_중립', diff --git a/packages/logic/src/triggers/special/personality/che_대의.ts b/packages/logic/src/actionModules/traits/personality/che_대의.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_대의.ts rename to packages/logic/src/actionModules/traits/personality/che_대의.ts index 0e7ee59..84c7f3d 100644 --- a/packages/logic/src/triggers/special/personality/che_대의.ts +++ b/packages/logic/src/actionModules/traits/personality/che_대의.ts @@ -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_대의', diff --git a/packages/logic/src/triggers/special/personality/che_안전.ts b/packages/logic/src/actionModules/traits/personality/che_안전.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_안전.ts rename to packages/logic/src/actionModules/traits/personality/che_안전.ts index ea42e82..975736e 100644 --- a/packages/logic/src/triggers/special/personality/che_안전.ts +++ b/packages/logic/src/actionModules/traits/personality/che_안전.ts @@ -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_안전', diff --git a/packages/logic/src/triggers/special/personality/che_왕좌.ts b/packages/logic/src/actionModules/traits/personality/che_왕좌.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_왕좌.ts rename to packages/logic/src/actionModules/traits/personality/che_왕좌.ts index 9a7cea6..ba1576d 100644 --- a/packages/logic/src/triggers/special/personality/che_왕좌.ts +++ b/packages/logic/src/actionModules/traits/personality/che_왕좌.ts @@ -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_왕좌', diff --git a/packages/logic/src/triggers/special/personality/che_유지.ts b/packages/logic/src/actionModules/traits/personality/che_유지.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_유지.ts rename to packages/logic/src/actionModules/traits/personality/che_유지.ts index de302be..4ba32bb 100644 --- a/packages/logic/src/triggers/special/personality/che_유지.ts +++ b/packages/logic/src/actionModules/traits/personality/che_유지.ts @@ -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_유지', diff --git a/packages/logic/src/triggers/special/personality/che_은둔.ts b/packages/logic/src/actionModules/traits/personality/che_은둔.ts similarity index 94% rename from packages/logic/src/triggers/special/personality/che_은둔.ts rename to packages/logic/src/actionModules/traits/personality/che_은둔.ts index bf3212d..70de064 100644 --- a/packages/logic/src/triggers/special/personality/che_은둔.ts +++ b/packages/logic/src/actionModules/traits/personality/che_은둔.ts @@ -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_은둔', diff --git a/packages/logic/src/triggers/special/personality/che_의협.ts b/packages/logic/src/actionModules/traits/personality/che_의협.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_의협.ts rename to packages/logic/src/actionModules/traits/personality/che_의협.ts index e85b26a..7266e5d 100644 --- a/packages/logic/src/triggers/special/personality/che_의협.ts +++ b/packages/logic/src/actionModules/traits/personality/che_의협.ts @@ -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_의협', diff --git a/packages/logic/src/triggers/special/personality/che_재간.ts b/packages/logic/src/actionModules/traits/personality/che_재간.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_재간.ts rename to packages/logic/src/actionModules/traits/personality/che_재간.ts index 6001a20..813ffbd 100644 --- a/packages/logic/src/triggers/special/personality/che_재간.ts +++ b/packages/logic/src/actionModules/traits/personality/che_재간.ts @@ -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_재간', diff --git a/packages/logic/src/triggers/special/personality/che_정복.ts b/packages/logic/src/actionModules/traits/personality/che_정복.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_정복.ts rename to packages/logic/src/actionModules/traits/personality/che_정복.ts index 7e458cc..4fd830e 100644 --- a/packages/logic/src/triggers/special/personality/che_정복.ts +++ b/packages/logic/src/actionModules/traits/personality/che_정복.ts @@ -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_정복', diff --git a/packages/logic/src/triggers/special/personality/che_출세.ts b/packages/logic/src/actionModules/traits/personality/che_출세.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_출세.ts rename to packages/logic/src/actionModules/traits/personality/che_출세.ts index 0d48d6a..c620317 100644 --- a/packages/logic/src/triggers/special/personality/che_출세.ts +++ b/packages/logic/src/actionModules/traits/personality/che_출세.ts @@ -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_출세', diff --git a/packages/logic/src/triggers/special/personality/che_패권.ts b/packages/logic/src/actionModules/traits/personality/che_패권.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_패권.ts rename to packages/logic/src/actionModules/traits/personality/che_패권.ts index cbdf8a4..d202dfd 100644 --- a/packages/logic/src/triggers/special/personality/che_패권.ts +++ b/packages/logic/src/actionModules/traits/personality/che_패권.ts @@ -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_패권', diff --git a/packages/logic/src/triggers/special/personality/che_할거.ts b/packages/logic/src/actionModules/traits/personality/che_할거.ts similarity index 93% rename from packages/logic/src/triggers/special/personality/che_할거.ts rename to packages/logic/src/actionModules/traits/personality/che_할거.ts index c5dfc8a..e134b9b 100644 --- a/packages/logic/src/triggers/special/personality/che_할거.ts +++ b/packages/logic/src/actionModules/traits/personality/che_할거.ts @@ -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_할거', diff --git a/packages/logic/src/triggers/special/personality/index.ts b/packages/logic/src/actionModules/traits/personality/index.ts similarity index 98% rename from packages/logic/src/triggers/special/personality/index.ts rename to packages/logic/src/actionModules/traits/personality/index.ts index 3492280..5491e2d 100644 --- a/packages/logic/src/triggers/special/personality/index.ts +++ b/packages/logic/src/actionModules/traits/personality/index.ts @@ -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_안전', diff --git a/packages/logic/src/triggers/special/requirements.ts b/packages/logic/src/actionModules/traits/requirements.ts similarity index 100% rename from packages/logic/src/triggers/special/requirements.ts rename to packages/logic/src/actionModules/traits/requirements.ts diff --git a/packages/logic/src/triggers/special/selector.ts b/packages/logic/src/actionModules/traits/selector.ts similarity index 100% rename from packages/logic/src/triggers/special/selector.ts rename to packages/logic/src/actionModules/traits/selector.ts diff --git a/packages/logic/src/triggers/special/types.ts b/packages/logic/src/actionModules/traits/types.ts similarity index 83% rename from packages/logic/src/triggers/special/types.ts rename to packages/logic/src/actionModules/traits/types.ts index db7ce3e..723b82b 100644 --- a/packages/logic/src/triggers/special/types.ts +++ b/packages/logic/src/actionModules/traits/types.ts @@ -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; } -export interface TraitModuleRegistry { +export interface TraitCatalog { domestic: Map>; war: Map>; personality: Map>; diff --git a/packages/logic/src/triggers/special/war/aux.ts b/packages/logic/src/actionModules/traits/war/aux.ts similarity index 100% rename from packages/logic/src/triggers/special/war/aux.ts rename to packages/logic/src/actionModules/traits/war/aux.ts diff --git a/packages/logic/src/triggers/special/war/che_격노.ts b/packages/logic/src/actionModules/traits/war/che_격노.ts similarity index 94% rename from packages/logic/src/triggers/special/war/che_격노.ts rename to packages/logic/src/actionModules/traits/war/che_격노.ts index 472c859..a66f454 100644 --- a/packages/logic/src/triggers/special/war/che_격노.ts +++ b/packages/logic/src/actionModules/traits/war/che_격노.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_견고.ts b/packages/logic/src/actionModules/traits/war/che_견고.ts similarity index 95% rename from packages/logic/src/triggers/special/war/che_견고.ts rename to packages/logic/src/actionModules/traits/war/che_견고.ts index 5daf301..2a08ce6 100644 --- a/packages/logic/src/triggers/special/war/che_견고.ts +++ b/packages/logic/src/actionModules/traits/war/che_견고.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_공성.ts b/packages/logic/src/actionModules/traits/war/che_공성.ts similarity index 96% rename from packages/logic/src/triggers/special/war/che_공성.ts rename to packages/logic/src/actionModules/traits/war/che_공성.ts index d2ce15e..239f582 100644 --- a/packages/logic/src/triggers/special/war/che_공성.ts +++ b/packages/logic/src/actionModules/traits/war/che_공성.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_궁병.ts b/packages/logic/src/actionModules/traits/war/che_궁병.ts similarity index 96% rename from packages/logic/src/triggers/special/war/che_궁병.ts rename to packages/logic/src/actionModules/traits/war/che_궁병.ts index b3574d1..536e166 100644 --- a/packages/logic/src/triggers/special/war/che_궁병.ts +++ b/packages/logic/src/actionModules/traits/war/che_궁병.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_귀병.ts b/packages/logic/src/actionModules/traits/war/che_귀병.ts similarity index 96% rename from packages/logic/src/triggers/special/war/che_귀병.ts rename to packages/logic/src/actionModules/traits/war/che_귀병.ts index 682a8de..e19977e 100644 --- a/packages/logic/src/triggers/special/war/che_귀병.ts +++ b/packages/logic/src/actionModules/traits/war/che_귀병.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_기병.ts b/packages/logic/src/actionModules/traits/war/che_기병.ts similarity index 96% rename from packages/logic/src/triggers/special/war/che_기병.ts rename to packages/logic/src/actionModules/traits/war/che_기병.ts index fcf751f..ae52eb7 100644 --- a/packages/logic/src/triggers/special/war/che_기병.ts +++ b/packages/logic/src/actionModules/traits/war/che_기병.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_돌격.ts b/packages/logic/src/actionModules/traits/war/che_돌격.ts similarity index 94% rename from packages/logic/src/triggers/special/war/che_돌격.ts rename to packages/logic/src/actionModules/traits/war/che_돌격.ts index 6ccb864..216e08e 100644 --- a/packages/logic/src/triggers/special/war/che_돌격.ts +++ b/packages/logic/src/actionModules/traits/war/che_돌격.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_무쌍.ts b/packages/logic/src/actionModules/traits/war/che_무쌍.ts similarity index 90% rename from packages/logic/src/triggers/special/war/che_무쌍.ts rename to packages/logic/src/actionModules/traits/war/che_무쌍.ts index 34def26..72c7acb 100644 --- a/packages/logic/src/triggers/special/war/che_무쌍.ts +++ b/packages/logic/src/actionModules/traits/war/che_무쌍.ts @@ -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, - 'rank_killnum', - 0 - ); + const killnum = getMetaNumber(unit.getGeneral().meta as Record, 'rank_killnum', 0); const logVal = Math.log2(Math.max(1, killnum / 5)); attackMultiplier += logVal / 20; defenceMultiplier -= logVal / 50; diff --git a/packages/logic/src/triggers/special/war/che_반계.ts b/packages/logic/src/actionModules/traits/war/che_반계.ts similarity index 95% rename from packages/logic/src/triggers/special/war/che_반계.ts rename to packages/logic/src/actionModules/traits/war/che_반계.ts index dfd8095..d03958c 100644 --- a/packages/logic/src/triggers/special/war/che_반계.ts +++ b/packages/logic/src/actionModules/traits/war/che_반계.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_보병.ts b/packages/logic/src/actionModules/traits/war/che_보병.ts similarity index 96% rename from packages/logic/src/triggers/special/war/che_보병.ts rename to packages/logic/src/actionModules/traits/war/che_보병.ts index 0bdbb00..7610e59 100644 --- a/packages/logic/src/triggers/special/war/che_보병.ts +++ b/packages/logic/src/actionModules/traits/war/che_보병.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_신산.ts b/packages/logic/src/actionModules/traits/war/che_신산.ts similarity index 94% rename from packages/logic/src/triggers/special/war/che_신산.ts rename to packages/logic/src/actionModules/traits/war/che_신산.ts index 7251498..f0e0ac3 100644 --- a/packages/logic/src/triggers/special/war/che_신산.ts +++ b/packages/logic/src/actionModules/traits/war/che_신산.ts @@ -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; diff --git a/packages/logic/src/triggers/special/war/che_신중.ts b/packages/logic/src/actionModules/traits/war/che_신중.ts similarity index 92% rename from packages/logic/src/triggers/special/war/che_신중.ts rename to packages/logic/src/actionModules/traits/war/che_신중.ts index ee5ae4b..b5bb691 100644 --- a/packages/logic/src/triggers/special/war/che_신중.ts +++ b/packages/logic/src/actionModules/traits/war/che_신중.ts @@ -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; diff --git a/packages/logic/src/triggers/special/war/che_위압.ts b/packages/logic/src/actionModules/traits/war/che_위압.ts similarity index 93% rename from packages/logic/src/triggers/special/war/che_위압.ts rename to packages/logic/src/actionModules/traits/war/che_위압.ts index 824c4a9..13f8618 100644 --- a/packages/logic/src/triggers/special/war/che_위압.ts +++ b/packages/logic/src/actionModules/traits/war/che_위압.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_의술.ts b/packages/logic/src/actionModules/traits/war/che_의술.ts similarity index 94% rename from packages/logic/src/triggers/special/war/che_의술.ts rename to packages/logic/src/actionModules/traits/war/che_의술.ts index eb0fc45..34ba29b 100644 --- a/packages/logic/src/triggers/special/war/che_의술.ts +++ b/packages/logic/src/actionModules/traits/war/che_의술.ts @@ -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'; // 전투 특기: 의술 diff --git a/packages/logic/src/triggers/special/war/che_저격.ts b/packages/logic/src/actionModules/traits/war/che_저격.ts similarity index 94% rename from packages/logic/src/triggers/special/war/che_저격.ts rename to packages/logic/src/actionModules/traits/war/che_저격.ts index 463cd49..6f726d6 100644 --- a/packages/logic/src/triggers/special/war/che_저격.ts +++ b/packages/logic/src/actionModules/traits/war/che_저격.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_집중.ts b/packages/logic/src/actionModules/traits/war/che_집중.ts similarity index 92% rename from packages/logic/src/triggers/special/war/che_집중.ts rename to packages/logic/src/actionModules/traits/war/che_집중.ts index 2f539de..347fa61 100644 --- a/packages/logic/src/triggers/special/war/che_집중.ts +++ b/packages/logic/src/actionModules/traits/war/che_집중.ts @@ -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; diff --git a/packages/logic/src/triggers/special/war/che_징병.ts b/packages/logic/src/actionModules/traits/war/che_징병.ts similarity index 95% rename from packages/logic/src/triggers/special/war/che_징병.ts rename to packages/logic/src/actionModules/traits/war/che_징병.ts index ef97864..498f922 100644 --- a/packages/logic/src/triggers/special/war/che_징병.ts +++ b/packages/logic/src/actionModules/traits/war/che_징병.ts @@ -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; diff --git a/packages/logic/src/triggers/special/war/che_척사.ts b/packages/logic/src/actionModules/traits/war/che_척사.ts similarity index 93% rename from packages/logic/src/triggers/special/war/che_척사.ts rename to packages/logic/src/actionModules/traits/war/che_척사.ts index 46093df..70a108d 100644 --- a/packages/logic/src/triggers/special/war/che_척사.ts +++ b/packages/logic/src/actionModules/traits/war/che_척사.ts @@ -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; diff --git a/packages/logic/src/triggers/special/war/che_필살.ts b/packages/logic/src/actionModules/traits/war/che_필살.ts similarity index 94% rename from packages/logic/src/triggers/special/war/che_필살.ts rename to packages/logic/src/actionModules/traits/war/che_필살.ts index f4a33e5..ab331f1 100644 --- a/packages/logic/src/triggers/special/war/che_필살.ts +++ b/packages/logic/src/actionModules/traits/war/che_필살.ts @@ -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'; diff --git a/packages/logic/src/triggers/special/war/che_환술.ts b/packages/logic/src/actionModules/traits/war/che_환술.ts similarity index 92% rename from packages/logic/src/triggers/special/war/che_환술.ts rename to packages/logic/src/actionModules/traits/war/che_환술.ts index 1f73dc5..6e8a942 100644 --- a/packages/logic/src/triggers/special/war/che_환술.ts +++ b/packages/logic/src/actionModules/traits/war/che_환술.ts @@ -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; diff --git a/packages/logic/src/triggers/special/war/index.ts b/packages/logic/src/actionModules/traits/war/index.ts similarity index 98% rename from packages/logic/src/triggers/special/war/index.ts rename to packages/logic/src/actionModules/traits/war/index.ts index 00e18bc..e477879 100644 --- a/packages/logic/src/triggers/special/war/index.ts +++ b/packages/logic/src/actionModules/traits/war/index.ts @@ -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_귀병', diff --git a/packages/logic/src/triggers/types.ts b/packages/logic/src/actionModules/types.ts similarity index 91% rename from packages/logic/src/triggers/types.ts rename to packages/logic/src/actionModules/types.ts index 16c98e2..6f3b553 100644 --- a/packages/logic/src/triggers/types.ts +++ b/packages/logic/src/actionModules/types.ts @@ -1,7 +1,3 @@ -export type TriggerActionType = '장비매매' | 'GeneralCommand'; - -export type TriggerActionPhase = '판매' | '구매'; - export type TriggerDomesticActionType = | '상업' | '농업' diff --git a/packages/logic/src/actions/turn/actionContext.ts b/packages/logic/src/actions/turn/actionContext.ts index 4c38ac6..11ec117 100644 --- a/packages/logic/src/actions/turn/actionContext.ts +++ b/packages/logic/src/actions/turn/actionContext.ts @@ -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; diff --git a/packages/logic/src/actions/turn/commandEnv.ts b/packages/logic/src/actions/turn/commandEnv.ts index 62f6814..6280c1e 100644 --- a/packages/logic/src/actions/turn/commandEnv.ts +++ b/packages/logic/src/actions/turn/commandEnv.ts @@ -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; - generalActionModules?: Array; - warActionModules?: Array; + generalActionModules?: RefOrderedActionStack; + warActionModules?: RefOrderedActionStack; nationTraitModules?: Array; } diff --git a/packages/logic/src/actions/turn/general/che_내정특기초기화.ts b/packages/logic/src/actions/turn/general/che_내정특기초기화.ts index 3b9db75..ce4fba8 100644 --- a/packages/logic/src/actions/turn/general/che_내정특기초기화.ts +++ b/packages/logic/src/actions/turn/general/che_내정특기초기화.ts @@ -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 {} diff --git a/packages/logic/src/actions/turn/general/che_모병.ts b/packages/logic/src/actions/turn/general/che_모병.ts index b416eb6..e39bd54 100644 --- a/packages/logic/src/actions/turn/general/che_모병.ts +++ b/packages/logic/src/actions/turn/general/che_모병.ts @@ -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[]) { + constructor(modules: ReadonlyArray>) { super(modules, { actionName: '모병', costOffset: 2, diff --git a/packages/logic/src/actions/turn/general/che_물자조달.ts b/packages/logic/src/actions/turn/general/che_물자조달.ts index a052526..1f50990 100644 --- a/packages/logic/src/actions/turn/general/che_물자조달.ts +++ b/packages/logic/src/actions/turn/general/che_물자조달.ts @@ -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; - constructor(modules: Array | null | undefined>) { + constructor(modules: ReadonlyArray | null | undefined>) { this.pipeline = new GeneralActionPipeline(modules); } @@ -186,7 +186,7 @@ export class ActionDefinition< public readonly name = ACTION_NAME; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>) { + constructor(modules: ReadonlyArray | null | undefined>) { this.resolver = new ActionResolver(modules); } diff --git a/packages/logic/src/actions/turn/general/che_사기진작.ts b/packages/logic/src/actions/turn/general/che_사기진작.ts index 033e187..819efe1 100644 --- a/packages/logic/src/actions/turn/general/che_사기진작.ts +++ b/packages/logic/src/actions/turn/general/che_사기진작.ts @@ -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 {} diff --git a/packages/logic/src/actions/turn/general/che_상업투자.ts b/packages/logic/src/actions/turn/general/che_상업투자.ts index 489966f..c962464 100644 --- a/packages/logic/src/actions/turn/general/che_상업투자.ts +++ b/packages/logic/src/actions/turn/general/che_상업투자.ts @@ -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 | null | undefined>, + modules: ReadonlyArray | null | undefined>, env: InvestmentEnvironment, config: InvestmentConfig = DEFAULT_CONFIG ) { @@ -324,7 +324,7 @@ export class ActionResolver< private readonly config: InvestmentConfig; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, env: InvestmentEnvironment, config: InvestmentConfig = DEFAULT_CONFIG ) { @@ -398,7 +398,7 @@ export class ActionDefinition< private readonly config: InvestmentConfig; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, env: InvestmentEnvironment, config: InvestmentConfig = DEFAULT_CONFIG ) { diff --git a/packages/logic/src/actions/turn/general/che_선동.ts b/packages/logic/src/actions/turn/general/che_선동.ts index 77cb928..cfca97a 100644 --- a/packages/logic/src/actions/turn/general/che_선동.ts +++ b/packages/logic/src/actions/turn/general/che_선동.ts @@ -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, diff --git a/packages/logic/src/actions/turn/general/che_소집해제.ts b/packages/logic/src/actions/turn/general/che_소집해제.ts index 8c0053f..244f80d 100644 --- a/packages/logic/src/actions/turn/general/che_소집해제.ts +++ b/packages/logic/src/actions/turn/general/che_소집해제.ts @@ -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 {} diff --git a/packages/logic/src/actions/turn/general/che_인재탐색.ts b/packages/logic/src/actions/turn/general/che_인재탐색.ts index edfd495..94d55f5 100644 --- a/packages/logic/src/actions/turn/general/che_인재탐색.ts +++ b/packages/logic/src/actions/turn/general/che_인재탐색.ts @@ -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 = (rng: RandomGenerator, values: readonly T[]): T => values[legacyChoiceIndex(rng, values.length)]!; +const legacyChoice = (rng: RandomGenerator, values: readonly T[]): T => + values[legacyChoiceIndex(rng, values.length)]!; const resolveCandidate = ( context: TalentScoutResolveContext, @@ -239,7 +238,10 @@ export class CommandResolver; private readonly env: TalentScoutEnvironment; - constructor(modules: Array | null | undefined>, env: TalentScoutEnvironment) { + constructor( + modules: ReadonlyArray | 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; - constructor(modules: Array | null | undefined>, env: TalentScoutEnvironment) { + constructor( + modules: ReadonlyArray | 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; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>, env: TalentScoutEnvironment) { + constructor( + modules: ReadonlyArray | 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), }; diff --git a/packages/logic/src/actions/turn/general/che_장비매매.ts b/packages/logic/src/actions/turn/general/che_장비매매.ts index de72276..60df674 100644 --- a/packages/logic/src/actions/turn/general/che_장비매매.ts +++ b/packages/logic/src/actions/turn/general/che_장비매매.ts @@ -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 { public readonly key = ACTION_KEY; public readonly name = ACTION_NAME; + private readonly pipeline: GeneralActionPipeline; - 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 { 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(`${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('item.purchased', { + itemKey: finalItemCode, + slot: itemType, + }) + ); + } else { + if (!context.time) { + throw new Error('장비 판매 이벤트에는 현재 연도와 시나리오 시작 연도가 필요합니다.'); + } + this.pipeline.dispatch( + { ...context, time: context.time, log: eventLog }, + createGeneralActionEvent('item.sold', { + itemKey: finalItemCode, + slot: itemType, + }) + ); + removeEquippedItem(general, itemType); + } + if (!buying && item && !item.buyable && nation) { const josaYi = JosaUtil.pick(general.name, '이'); context.addLog(`${general.name}${josaYi} ${itemName}${josaUl} 판매했습니다!`, { @@ -201,13 +240,31 @@ export class ActionDefinition< tryApplyUniqueLottery(context, { acquireType: '아이템', reason: ACTION_NAME }); + const effects: Array> = [ + createGeneralPatchEffect({ + 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({ - gold: nextGold, - experience: general.experience + 10, - }), - ], + effects, }; } } diff --git a/packages/logic/src/actions/turn/general/che_전투특기초기화.ts b/packages/logic/src/actions/turn/general/che_전투특기초기화.ts index f9dbe1c..e77f748 100644 --- a/packages/logic/src/actions/turn/general/che_전투특기초기화.ts +++ b/packages/logic/src/actions/turn/general/che_전투특기초기화.ts @@ -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 {} diff --git a/packages/logic/src/actions/turn/general/che_징병.ts b/packages/logic/src/actions/turn/general/che_징병.ts index 901fe2c..23b6c90 100644 --- a/packages/logic/src/actions/turn/general/che_징병.ts +++ b/packages/logic/src/actions/turn/general/che_징병.ts @@ -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; private readonly env: RecruitEnvironment; - constructor(modules: Array | null | undefined>, env: RecruitEnvironment) { + constructor(modules: ReadonlyArray | 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; - constructor(modules: Array | null | undefined>, env: RecruitEnvironment) { + constructor(modules: ReadonlyArray | null | undefined>, env: RecruitEnvironment) { this.env = env; this.command = new CommandResolver(modules, env); } @@ -444,7 +444,7 @@ export class ActionDefinition< private readonly resolver: ActionResolver; private readonly env: RecruitEnvironment; - constructor(modules: Array | null | undefined>, env: RecruitEnvironment) { + constructor(modules: ReadonlyArray | null | undefined>, env: RecruitEnvironment) { this.command = new CommandResolver(modules, env); this.resolver = new ActionResolver(modules, env); this.env = env; diff --git a/packages/logic/src/actions/turn/general/che_출병.ts b/packages/logic/src/actions/turn/general/che_출병.ts index 3cc4f26..c6fbede 100644 --- a/packages/logic/src/actions/turn/general/che_출병.ts +++ b/packages/logic/src/actions/turn/general/che_출병.ts @@ -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>; + private readonly warModules: ReadonlyArray>; private readonly nationTraitModules: Map; + private readonly generalModules: ReadonlyArray>; constructor( - modules: Array | null | undefined> = [], - nationTraitModules: NationTraitModule[] = [] + modules: ReadonlyArray | null | undefined> = [], + nationTraitModules: NationTraitModule[] = [], + generalModules: ReadonlyArray | null | undefined> = [] ) { - this.warModules = modules.filter(Boolean) as Array>; + this.warModules = modules.filter(Boolean) as ReadonlyArray>; this.nationTraitModules = new Map(nationTraitModules.map((module) => [module.key, module])); + this.generalModules = generalModules.filter(Boolean) as ReadonlyArray>; } 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 ?? []), }; diff --git a/packages/logic/src/actions/turn/general/che_탈취.ts b/packages/logic/src/actions/turn/general/che_탈취.ts index 7084481..40fd74f 100644 --- a/packages/logic/src/actions/turn/general/che_탈취.ts +++ b/packages/logic/src/actions/turn/general/che_탈취.ts @@ -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, diff --git a/packages/logic/src/actions/turn/general/che_파괴.ts b/packages/logic/src/actions/turn/general/che_파괴.ts index b208b70..ae9aa0d 100644 --- a/packages/logic/src/actions/turn/general/che_파괴.ts +++ b/packages/logic/src/actions/turn/general/che_파괴.ts @@ -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, diff --git a/packages/logic/src/actions/turn/general/che_화계.ts b/packages/logic/src/actions/turn/general/che_화계.ts index c19f112..4efdd67 100644 --- a/packages/logic/src/actions/turn/general/che_화계.ts +++ b/packages/logic/src/actions/turn/general/che_화계.ts @@ -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 | null | undefined>, env: FireAttackEnvironment) { + constructor( + modules: ReadonlyArray | 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; private readonly pipeline: GeneralActionPipeline; - constructor(modules: Array | null | undefined>, env: FireAttackEnvironment) { + constructor( + modules: ReadonlyArray | 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(`${itemCode}${JosaUtil.pick(itemCode, '을')} 사용!`, { format: LogFormat.PLAIN, @@ -405,7 +410,10 @@ export class ActionDefinition< private readonly command: CommandResolver; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>, env: FireAttackEnvironment) { + constructor( + modules: ReadonlyArray | null | undefined>, + env: FireAttackEnvironment + ) { this.command = new CommandResolver(modules, env); this.resolver = new ActionResolver(modules, env); } diff --git a/packages/logic/src/actions/turn/general/cr_맹훈련.ts b/packages/logic/src/actions/turn/general/cr_맹훈련.ts index e2e923f..ee02034 100644 --- a/packages/logic/src/actions/turn/general/cr_맹훈련.ts +++ b/packages/logic/src/actions/turn/general/cr_맹훈련.ts @@ -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_맹훈련'; diff --git a/packages/logic/src/actions/turn/general/foundingShared.ts b/packages/logic/src/actions/turn/general/foundingShared.ts index 9e67998..abab030 100644 --- a/packages/logic/src/actions/turn/general/foundingShared.ts +++ b/packages/logic/src/actions/turn/general/foundingShared.ts @@ -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'; diff --git a/packages/logic/src/actions/turn/general/strategyItemConsumption.ts b/packages/logic/src/actions/turn/general/strategyItemConsumption.ts index b8cffba..9383c9d 100644 --- a/packages/logic/src/actions/turn/general/strategyItemConsumption.ts +++ b/packages/logic/src/actions/turn/general/strategyItemConsumption.ts @@ -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 = ( pipeline: GeneralActionPipeline, context: GeneralActionResolveContext -): Record | null => - pipeline.onArbitraryAction(context, 'GeneralCommand', null, { - command: '계략', - success: true, - }); +): readonly string[] => + pipeline.dispatch( + context, + createGeneralActionEvent('strategy.succeeded', { + consumedItems: [], + }) + ).payload.consumedItems; diff --git a/packages/logic/src/actions/turn/nation/che_급습.ts b/packages/logic/src/actions/turn/nation/che_급습.ts index b7e4542..0235b9a 100644 --- a/packages/logic/src/actions/turn/nation/che_급습.ts +++ b/packages/logic/src/actions/turn/nation/che_급습.ts @@ -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; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | 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; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | 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; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/actions/turn/nation/che_백성동원.ts b/packages/logic/src/actions/turn/nation/che_백성동원.ts index 1c30f82..bbde89b 100644 --- a/packages/logic/src/actions/turn/nation/che_백성동원.ts +++ b/packages/logic/src/actions/turn/nation/che_백성동원.ts @@ -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; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | 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; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | 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; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/actions/turn/nation/che_수몰.ts b/packages/logic/src/actions/turn/nation/che_수몰.ts index 11c510a..a07c9c6 100644 --- a/packages/logic/src/actions/turn/nation/che_수몰.ts +++ b/packages/logic/src/actions/turn/nation/che_수몰.ts @@ -8,7 +8,7 @@ import { notOccupiedDestCity, 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, @@ -56,7 +56,7 @@ export class CommandResolver; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, private readonly initialNationGenLimit = 10 ) { this.pipeline = new GeneralActionPipeline(modules); @@ -86,7 +86,10 @@ export class ActionResolver< readonly key = 'che_수몰'; private readonly command: CommandResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.command = new CommandResolver(modules, initialNationGenLimit); } @@ -194,7 +197,10 @@ export class ActionDefinition< public readonly name = ACTION_NAME; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/actions/turn/nation/che_의병모집.ts b/packages/logic/src/actions/turn/nation/che_의병모집.ts index 13383da..1f3b41a 100644 --- a/packages/logic/src/actions/turn/nation/che_의병모집.ts +++ b/packages/logic/src/actions/turn/nation/che_의병모집.ts @@ -14,7 +14,7 @@ import { notOpeningPart, 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, @@ -238,7 +238,7 @@ export class CommandResolver | null | undefined>, + modules: ReadonlyArray | null | undefined>, env: VolunteerRecruitEnvironment ) { this.pipeline = new GeneralActionPipeline(modules); @@ -272,7 +272,7 @@ export class ActionResolver< private readonly command: CommandResolver; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, env: VolunteerRecruitEnvironment ) { this.env = env; @@ -461,7 +461,7 @@ export class ActionDefinition< private readonly env: VolunteerRecruitEnvironment; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, env: VolunteerRecruitEnvironment ) { this.env = env; diff --git a/packages/logic/src/actions/turn/nation/che_이호경식.ts b/packages/logic/src/actions/turn/nation/che_이호경식.ts index 87fb29e..d51b72a 100644 --- a/packages/logic/src/actions/turn/nation/che_이호경식.ts +++ b/packages/logic/src/actions/turn/nation/che_이호경식.ts @@ -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, @@ -52,7 +52,7 @@ export class CommandResolver; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, private readonly initialNationGenLimit = 10 ) { this.pipeline = new GeneralActionPipeline(modules); @@ -76,7 +76,10 @@ export class ActionResolver< readonly key = 'che_이호경식'; private readonly command: CommandResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.command = new CommandResolver(modules, initialNationGenLimit); } @@ -184,7 +187,10 @@ export class ActionDefinition< public readonly name = ACTION_NAME; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/actions/turn/nation/che_피장파장.ts b/packages/logic/src/actions/turn/nation/che_피장파장.ts index 7024298..255c5db 100644 --- a/packages/logic/src/actions/turn/nation/che_피장파장.ts +++ b/packages/logic/src/actions/turn/nation/che_피장파장.ts @@ -8,7 +8,7 @@ import { occupiedCity, } from '@sammo-ts/logic/constraints/presets.js'; import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.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, @@ -123,7 +123,7 @@ export class ActionResolver< private readonly pipeline: GeneralActionPipeline; constructor( - modules: Array | null | undefined> = [], + modules: ReadonlyArray | null | undefined> = [], private readonly initialNationGenLimit = 10 ) { this.pipeline = new GeneralActionPipeline(modules); @@ -263,7 +263,10 @@ export class ActionDefinition< public readonly name = ACTION_NAME; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined> = [], initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined> = [], + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/actions/turn/nation/che_필사즉생.ts b/packages/logic/src/actions/turn/nation/che_필사즉생.ts index a0f017a..53c7893 100644 --- a/packages/logic/src/actions/turn/nation/che_필사즉생.ts +++ b/packages/logic/src/actions/turn/nation/che_필사즉생.ts @@ -6,7 +6,7 @@ import { beChief, 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, @@ -41,7 +41,7 @@ export class CommandResolver; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, private readonly initialNationGenLimit = 10 ) { this.pipeline = new GeneralActionPipeline(modules); @@ -65,7 +65,10 @@ export class ActionResolver< readonly key = 'che_필사즉생'; private readonly command: CommandResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.command = new CommandResolver(modules, initialNationGenLimit); } @@ -155,7 +158,10 @@ export class ActionDefinition< public readonly name = ACTION_NAME; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/actions/turn/nation/che_허보.ts b/packages/logic/src/actions/turn/nation/che_허보.ts index e4403b7..47ca047 100644 --- a/packages/logic/src/actions/turn/nation/che_허보.ts +++ b/packages/logic/src/actions/turn/nation/che_허보.ts @@ -8,7 +8,7 @@ import { notOccupiedDestCity, 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, @@ -72,7 +72,7 @@ export class CommandResolver; constructor( - modules: Array | null | undefined>, + modules: ReadonlyArray | null | undefined>, private readonly initialNationGenLimit = 10 ) { this.pipeline = new GeneralActionPipeline(modules); @@ -96,7 +96,10 @@ export class ActionResolver< readonly key = 'che_허보'; private readonly command: CommandResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.command = new CommandResolver(modules, initialNationGenLimit); } @@ -195,7 +198,10 @@ export class ActionDefinition< public readonly name = ACTION_NAME; private readonly resolver: ActionResolver; - constructor(modules: Array | null | undefined>, initialNationGenLimit = 10) { + constructor( + modules: ReadonlyArray | null | undefined>, + initialNationGenLimit = 10 + ) { this.resolver = new ActionResolver(modules, initialNationGenLimit); } diff --git a/packages/logic/src/crewType/catalog.ts b/packages/logic/src/crewType/catalog.ts index ceb9631..700dd6c 100644 --- a/packages/logic/src/crewType/catalog.ts +++ b/packages/logic/src/crewType/catalog.ts @@ -1,7 +1,12 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; import { GeneralTriggerCaller } 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 { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js'; import { WarTriggerCaller, type WarTriggerRegistry } from '@sammo-ts/logic/war/triggers.js'; import type { CrewTypeDefinition, CrewTypeRequirement, UnitSetDefinition } from '@sammo-ts/logic/world/types.js'; @@ -109,6 +114,16 @@ const createGeneralActionRouter = ( (byId.get(context.general.crewTypeId)?.actions ?? []) .map((action) => action.general as GeneralActionModule | undefined) .filter((action): action is GeneralActionModule => action !== undefined); + const handleEvent = ( + context: GeneralActionEventContext, + event: GeneralActionEvent + ): GeneralActionEvent => { + let current = event; + for (const module of modules(context)) { + current = dispatchGeneralActionModuleEvent(module, context, current); + } + return current; + }; return { getPreTurnExecuteTriggerList: (context) => { @@ -153,13 +168,7 @@ const createGeneralActionRouter = ( } return current; }, - onArbitraryAction: (context, actionType, phase, aux) => { - let current = aux ?? null; - for (const module of modules(context)) { - current = module.onArbitraryAction?.(context, actionType, phase, current) ?? current; - } - return current; - }, + handleEvent, } satisfies GeneralActionModule; }; diff --git a/packages/logic/src/crewType/types.ts b/packages/logic/src/crewType/types.ts index d2208e0..dea005f 100644 --- a/packages/logic/src/crewType/types.ts +++ b/packages/logic/src/crewType/types.ts @@ -1,4 +1,4 @@ -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 { CrewTypeDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js'; diff --git a/packages/logic/src/economy/nationIncome.ts b/packages/logic/src/economy/nationIncome.ts index 0651bcf..624943e 100644 --- a/packages/logic/src/economy/nationIncome.ts +++ b/packages/logic/src/economy/nationIncome.ts @@ -1,6 +1,6 @@ import type { General, Nation } from '../domain/entities.js'; import type { GeneralActionContext } from '../triggers/general.js'; -import type { TriggerNationalIncomeType } from '../triggers/types.js'; +import type { TriggerNationalIncomeType } from '../actionModules/types.js'; export type NationIncomeModifier = (type: TriggerNationalIncomeType, amount: number) => number; diff --git a/packages/logic/src/index.ts b/packages/logic/src/index.ts index 2bdbf6c..16e583b 100644 --- a/packages/logic/src/index.ts +++ b/packages/logic/src/index.ts @@ -1,6 +1,7 @@ export * from './domain/entities.js'; export type { RandomGenerator } from '@sammo-ts/common'; export * from './actions/index.js'; +export * from './actionModules/index.js'; export * from './auction/alias.js'; export * from './auction/neutral.js'; export * from './constraints/index.js'; diff --git a/packages/logic/src/inheritance/inheritBuff.ts b/packages/logic/src/inheritance/inheritBuff.ts index f80f3f2..deb1a96 100644 --- a/packages/logic/src/inheritance/inheritBuff.ts +++ b/packages/logic/src/inheritance/inheritBuff.ts @@ -1,5 +1,9 @@ -import type { TriggerDomesticActionType, TriggerDomesticVarType, WarStatName } from '@sammo-ts/logic/triggers/types.js'; -import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js'; +import type { + TriggerDomesticActionType, + TriggerDomesticVarType, + WarStatName, +} from '@sammo-ts/logic/actionModules/types.js'; +import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js'; import type { WarActionModule } from '@sammo-ts/logic/war/actions.js'; import { asRecord, parseJson } from '@sammo-ts/common'; diff --git a/packages/logic/src/items/base.ts b/packages/logic/src/items/base.ts index 2e97847..b1ef19d 100644 --- a/packages/logic/src/items/base.ts +++ b/packages/logic/src/items/base.ts @@ -1,5 +1,5 @@ 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 { ItemModule, ItemSlot } from './types.js'; diff --git a/packages/logic/src/items/che_간파_노군입산부.ts b/packages/logic/src/items/che_간파_노군입산부.ts index 2008d5d..9e2317b 100644 --- a/packages/logic/src/items/che_간파_노군입산부.ts +++ b/packages/logic/src/items/che_간파_노군입산부.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_계략_삼략.ts b/packages/logic/src/items/che_계략_삼략.ts index 4066963..82ef7d6 100644 --- a/packages/logic/src/items/che_계략_삼략.ts +++ b/packages/logic/src/items/che_계략_삼략.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_계략_육도.ts b/packages/logic/src/items/che_계략_육도.ts index 6f1e072..604b6b1 100644 --- a/packages/logic/src/items/che_계략_육도.ts +++ b/packages/logic/src/items/che_계략_육도.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_계략_이추.ts b/packages/logic/src/items/che_계략_이추.ts index 8a4715d..2128980 100644 --- a/packages/logic/src/items/che_계략_이추.ts +++ b/packages/logic/src/items/che_계략_이추.ts @@ -13,7 +13,7 @@ export const itemModule: ItemModule = { consumable: true, reqSecu: 1000, unique: false, - consumeOn: [{ actionType: 'GeneralCommand', command: '계략', successOnly: true }], + consumeOnStrategySuccess: true, onCalcDomestic: (_context, turnType, varType, value) => { if (turnType === '계략' && varType === 'success') { return value + 0.2; diff --git a/packages/logic/src/items/che_계략_향낭.ts b/packages/logic/src/items/che_계략_향낭.ts index cdccb98..abc2347 100644 --- a/packages/logic/src/items/che_계략_향낭.ts +++ b/packages/logic/src/items/che_계략_향낭.ts @@ -13,7 +13,7 @@ export const itemModule: ItemModule = { consumable: true, reqSecu: 2000, unique: false, - consumeOn: [{ actionType: 'GeneralCommand', command: '계략', successOnly: true }], + consumeOnStrategySuccess: true, onCalcDomestic: (_context, turnType, varType, value) => { if (turnType === '계략' && varType === 'success') { return value + 0.5; diff --git a/packages/logic/src/items/che_농성_위공자병법.ts b/packages/logic/src/items/che_농성_위공자병법.ts index e3cd222..8ae3e2c 100644 --- a/packages/logic/src/items/che_농성_위공자병법.ts +++ b/packages/logic/src/items/che_농성_위공자병법.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_농성_주서음부.ts b/packages/logic/src/items/che_농성_주서음부.ts index 3388bff..b0e89a3 100644 --- a/packages/logic/src/items/che_농성_주서음부.ts +++ b/packages/logic/src/items/che_농성_주서음부.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_능력치_무력_두강주.ts b/packages/logic/src/items/che_능력치_무력_두강주.ts index cb0eee1..7d1a207 100644 --- a/packages/logic/src/items/che_능력치_무력_두강주.ts +++ b/packages/logic/src/items/che_능력치_무력_두강주.ts @@ -1,4 +1,4 @@ -import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js'; import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; import { clamp } from '@sammo-ts/logic/war/utils.js'; diff --git a/packages/logic/src/items/che_능력치_지력_이강주.ts b/packages/logic/src/items/che_능력치_지력_이강주.ts index 5390a19..0c09338 100644 --- a/packages/logic/src/items/che_능력치_지력_이강주.ts +++ b/packages/logic/src/items/che_능력치_지력_이강주.ts @@ -1,4 +1,4 @@ -import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js'; import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; import { clamp } from '@sammo-ts/logic/war/utils.js'; diff --git a/packages/logic/src/items/che_능력치_통솔_보령압주.ts b/packages/logic/src/items/che_능력치_통솔_보령압주.ts index 3c97074..47d49b6 100644 --- a/packages/logic/src/items/che_능력치_통솔_보령압주.ts +++ b/packages/logic/src/items/che_능력치_통솔_보령압주.ts @@ -1,4 +1,4 @@ -import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules/types.js'; import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; import { clamp } from '@sammo-ts/logic/war/utils.js'; diff --git a/packages/logic/src/items/che_명마_07_기주마.ts b/packages/logic/src/items/che_명마_07_기주마.ts index 7f6078a..57cbc49 100644 --- a/packages/logic/src/items/che_명마_07_기주마.ts +++ b/packages/logic/src/items/che_명마_07_기주마.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_명마_07_백상.ts b/packages/logic/src/items/che_명마_07_백상.ts index 5b6e877..5423b60 100644 --- a/packages/logic/src/items/che_명마_07_백상.ts +++ b/packages/logic/src/items/che_명마_07_백상.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_명마_12_옥란백용구.ts b/packages/logic/src/items/che_명마_12_옥란백용구.ts index 8045b7a..be00a2b 100644 --- a/packages/logic/src/items/che_명마_12_옥란백용구.ts +++ b/packages/logic/src/items/che_명마_12_옥란백용구.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; @@ -33,10 +33,7 @@ export const itemModule: ItemModule = { } if (statName === 'warAvoidRatio' && typeof newValue === 'number') { const leadership = - typeof aux === 'object' && - aux !== null && - 'leadership' in aux && - typeof aux.leadership === 'number' + typeof aux === 'object' && aux !== null && 'leadership' in aux && typeof aux.leadership === 'number' ? aux.leadership : context.general.stats.leadership + STAT_VALUE; const crewL = context.general.crew / 100; diff --git a/packages/logic/src/items/che_명성_구석.ts b/packages/logic/src/items/che_명성_구석.ts index 528f546..d41d508 100644 --- a/packages/logic/src/items/che_명성_구석.ts +++ b/packages/logic/src/items/che_명성_구석.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_보물_도기.ts b/packages/logic/src/items/che_보물_도기.ts index 04db9b1..27303e9 100644 --- a/packages/logic/src/items/che_보물_도기.ts +++ b/packages/logic/src/items/che_보물_도기.ts @@ -1,4 +1,3 @@ -import { JosaUtil } from '@sammo-ts/common'; import type { ItemModule } from './types.js'; const ITEM_KEY = 'che_보물_도기'; @@ -17,46 +16,32 @@ export const itemModule: ItemModule = { consumable: false, reqSecu: 0, unique: true, - onArbitraryAction: (context, actionType, phase, aux) => { - if (!aux || actionType !== '장비매매' || phase !== '판매') { - return aux ?? null; - } - if (aux['itemKey'] !== ITEM_KEY && aux['itemCode'] !== ITEM_KEY) { - return aux; - } - - const year = resolveNumber(aux['year']); - const startYear = resolveNumber(aux['startYear']); - const relYear = Math.max(0, year - startYear); - const score = Math.round(10000 + 5000 * Math.floor(relYear / 2)); - - const rng = context.rng; - const pick = rng?.nextBool(0.5) ?? true; - const resName = pick ? '금' : '쌀'; - const resKey = pick ? 'gold' : 'rice'; - - const nation = aux['nation']; - if (nation && typeof nation === 'object') { - const cast = nation as { gold?: number; rice?: number }; - const half = Math.floor(score / 2); - if (resKey === 'gold') { - cast.gold = (cast.gold ?? 0) + half; - } else { - cast.rice = (cast.rice ?? 0) + half; + eventHandlers: { + 'item.sold': (context, event) => { + if (event.payload.itemKey !== ITEM_KEY) { + return event; } - } - const selfGain = score - Math.floor(score / 2); - if (resKey === 'gold') { - context.general.gold += selfGain; - } else { - context.general.rice += selfGain; - } + const year = resolveNumber(context.time.year); + const startYear = resolveNumber(context.time.startYear); + const relYear = Math.max(0, year - startYear); + const score = Math.round(10000 + 5000 * Math.floor(relYear / 2)); - const josa = JosaUtil.pick('도기', '을'); - context.log?.push( - `${itemModule.name}${josa} 판매하여 ${resName} ${score.toLocaleString('en-US')}을 보충합니다.` - ); - return aux; + // ref RandUtil::choice([gold, rice])는 index 0을 금으로 고릅니다. + const pickGold = context.rng.nextInt(0, 2) === 0; + const resName = pickGold ? '금' : '쌀'; + const resKey = pickGold ? 'gold' : 'rice'; + + const nation = context.nation; + if (nation && nation.id !== 0) { + const half = Math.floor(score / 2); + nation[resKey] += half; + } + + const selfGain = score - Math.floor(score / 2); + context.general[resKey] += selfGain; + context.log?.push(`재산과 국고에 총 ${resName} ${score.toLocaleString('en-US')}을 보충합니다.`); + return event; + }, }, }; diff --git a/packages/logic/src/items/che_부적_태현청생부.ts b/packages/logic/src/items/che_부적_태현청생부.ts index 6fda3c6..1af7282 100644 --- a/packages/logic/src/items/che_부적_태현청생부.ts +++ b/packages/logic/src/items/che_부적_태현청생부.ts @@ -1,5 +1,5 @@ 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 { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; import { che_부적 } from '@sammo-ts/logic/war/triggers/che_부적.js'; diff --git a/packages/logic/src/items/che_사기_초선화.ts b/packages/logic/src/items/che_사기_초선화.ts index bc37d95..9d48a0c 100644 --- a/packages/logic/src/items/che_사기_초선화.ts +++ b/packages/logic/src/items/che_사기_초선화.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_사기_춘화첩.ts b/packages/logic/src/items/che_사기_춘화첩.ts index f0715ba..4f35045 100644 --- a/packages/logic/src/items/che_사기_춘화첩.ts +++ b/packages/logic/src/items/che_사기_춘화첩.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_01_효경전.ts b/packages/logic/src/items/che_서적_01_효경전.ts index 630f4fa..21807a2 100644 --- a/packages/logic/src/items/che_서적_01_효경전.ts +++ b/packages/logic/src/items/che_서적_01_효경전.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_02_회남자.ts b/packages/logic/src/items/che_서적_02_회남자.ts index 897fa5d..b3d5b98 100644 --- a/packages/logic/src/items/che_서적_02_회남자.ts +++ b/packages/logic/src/items/che_서적_02_회남자.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_03_변도론.ts b/packages/logic/src/items/che_서적_03_변도론.ts index 7a7ab60..2b27fc6 100644 --- a/packages/logic/src/items/che_서적_03_변도론.ts +++ b/packages/logic/src/items/che_서적_03_변도론.ts @@ -1,7 +1,7 @@ import { createStatItemModule } from './base.js'; import type { ItemModule } from './types.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'; const STAT_VALUE = 3; diff --git a/packages/logic/src/items/che_서적_04_건상역주.ts b/packages/logic/src/items/che_서적_04_건상역주.ts index 43e4995..45e51dd 100644 --- a/packages/logic/src/items/che_서적_04_건상역주.ts +++ b/packages/logic/src/items/che_서적_04_건상역주.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_05_여씨춘추.ts b/packages/logic/src/items/che_서적_05_여씨춘추.ts index 42e786c..17b9c81 100644 --- a/packages/logic/src/items/che_서적_05_여씨춘추.ts +++ b/packages/logic/src/items/che_서적_05_여씨춘추.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_06_사민월령.ts b/packages/logic/src/items/che_서적_06_사민월령.ts index 4247895..2cb8dd3 100644 --- a/packages/logic/src/items/che_서적_06_사민월령.ts +++ b/packages/logic/src/items/che_서적_06_사민월령.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_07_논어.ts b/packages/logic/src/items/che_서적_07_논어.ts index e1a4e13..d3ee409 100644 --- a/packages/logic/src/items/che_서적_07_논어.ts +++ b/packages/logic/src/items/che_서적_07_논어.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_07_위료자.ts b/packages/logic/src/items/che_서적_07_위료자.ts index 5c00e5a..6a0f363 100644 --- a/packages/logic/src/items/che_서적_07_위료자.ts +++ b/packages/logic/src/items/che_서적_07_위료자.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_서적_08_전론.ts b/packages/logic/src/items/che_서적_08_전론.ts index c24af76..a7528b6 100644 --- a/packages/logic/src/items/che_서적_08_전론.ts +++ b/packages/logic/src/items/che_서적_08_전론.ts @@ -1,7 +1,7 @@ import { createStatItemModule } from './base.js'; import type { ItemModule } from './types.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'; const STAT_VALUE = 8; diff --git a/packages/logic/src/items/che_서적_11_춘추전.ts b/packages/logic/src/items/che_서적_11_춘추전.ts index ab609c1..ab15538 100644 --- a/packages/logic/src/items/che_서적_11_춘추전.ts +++ b/packages/logic/src/items/che_서적_11_춘추전.ts @@ -1,5 +1,5 @@ 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 { createStatItemModule } from './base.js'; import type { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_숙련_동작.ts b/packages/logic/src/items/che_숙련_동작.ts index f0c22a9..b498ae4 100644 --- a/packages/logic/src/items/che_숙련_동작.ts +++ b/packages/logic/src/items/che_숙련_동작.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_집중_전국책.ts b/packages/logic/src/items/che_집중_전국책.ts index 87dc2fb..b1924fa 100644 --- a/packages/logic/src/items/che_집중_전국책.ts +++ b/packages/logic/src/items/che_집중_전국책.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_징병_낙주.ts b/packages/logic/src/items/che_징병_낙주.ts index 006f5c6..372bbbd 100644 --- a/packages/logic/src/items/che_징병_낙주.ts +++ b/packages/logic/src/items/che_징병_낙주.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_필살_둔갑천서.ts b/packages/logic/src/items/che_필살_둔갑천서.ts index 614d8aa..eba7e91 100644 --- a/packages/logic/src/items/che_필살_둔갑천서.ts +++ b/packages/logic/src/items/che_필살_둔갑천서.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_행동_서촉지형도.ts b/packages/logic/src/items/che_행동_서촉지형도.ts index e32ccb8..fa0205b 100644 --- a/packages/logic/src/items/che_행동_서촉지형도.ts +++ b/packages/logic/src/items/che_행동_서촉지형도.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_환술_논어집해.ts b/packages/logic/src/items/che_환술_논어집해.ts index 63a3dfe..abd63c4 100644 --- a/packages/logic/src/items/che_환술_논어집해.ts +++ b/packages/logic/src/items/che_환술_논어집해.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_회피_태평요술.ts b/packages/logic/src/items/che_회피_태평요술.ts index b7fe615..408b534 100644 --- a/packages/logic/src/items/che_회피_태평요술.ts +++ b/packages/logic/src/items/che_회피_태평요술.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_훈련_단결도.ts b/packages/logic/src/items/che_훈련_단결도.ts index f1befdd..292180c 100644 --- a/packages/logic/src/items/che_훈련_단결도.ts +++ b/packages/logic/src/items/che_훈련_단결도.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/che_훈련_철벽서.ts b/packages/logic/src/items/che_훈련_철벽서.ts index 8e0d552..9b5b63c 100644 --- a/packages/logic/src/items/che_훈련_철벽서.ts +++ b/packages/logic/src/items/che_훈련_철벽서.ts @@ -1,5 +1,5 @@ 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 { ItemModule } from './types.js'; diff --git a/packages/logic/src/items/eventBattleTrait.ts b/packages/logic/src/items/eventBattleTrait.ts index f0627dc..e45fc16 100644 --- a/packages/logic/src/items/eventBattleTrait.ts +++ b/packages/logic/src/items/eventBattleTrait.ts @@ -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'; import type { ItemModule } from './types.js'; export const createEventBattleTraitItemModule = ( @@ -38,8 +38,8 @@ export const createEventBattleTraitItemModule = ( if (traitModule.onCalcNationalIncome) { itemModule.onCalcNationalIncome = traitModule.onCalcNationalIncome; } - if (traitModule.onArbitraryAction) { - itemModule.onArbitraryAction = traitModule.onArbitraryAction; + if (traitModule.eventHandlers) { + itemModule.eventHandlers = traitModule.eventHandlers; } if (traitModule.getBattleInitTriggerList) { itemModule.getBattleInitTriggerList = traitModule.getBattleInitTriggerList; diff --git a/packages/logic/src/items/index.ts b/packages/logic/src/items/index.ts index 5383c5d..2af8e05 100644 --- a/packages/logic/src/items/index.ts +++ b/packages/logic/src/items/index.ts @@ -1,17 +1,22 @@ 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 { + createGeneralActionEvent, + dispatchGeneralActionEventHandlers, + type GeneralActionEvent, + type GeneralActionEventContext, + type GeneralActionEventType, +} from '@sammo-ts/logic/actionModules/events.js'; import { GeneralTriggerCaller, type GeneralActionContext } from '@sammo-ts/logic/triggers/general.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 { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; import type { WarUnit } from '@sammo-ts/logic/war/units.js'; @@ -275,7 +280,7 @@ const defaultImporters: Record = { che_약탈_옥벽: async () => import('./che_약탈_옥벽.js'), che_위압_조목삭: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_위압.js'), + import('../actionModules/traits/war/che_위압.js'), import('./eventBattleTrait.js'), ]); return { @@ -312,140 +317,140 @@ const defaultImporters: Record = { che_훈련_청주: async () => import('./che_훈련_청주.js'), event_전투특기_격노: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_격노.js'), + import('../actionModules/traits/war/che_격노.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_격노', traitModule) }; }, event_전투특기_견고: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_견고.js'), + import('../actionModules/traits/war/che_견고.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_견고', traitModule) }; }, event_전투특기_공성: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_공성.js'), + import('../actionModules/traits/war/che_공성.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_공성', traitModule) }; }, event_전투특기_궁병: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_궁병.js'), + import('../actionModules/traits/war/che_궁병.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_궁병', traitModule) }; }, event_전투특기_귀병: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_귀병.js'), + import('../actionModules/traits/war/che_귀병.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_귀병', traitModule) }; }, event_전투특기_기병: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_기병.js'), + import('../actionModules/traits/war/che_기병.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_기병', traitModule) }; }, event_전투특기_돌격: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_돌격.js'), + import('../actionModules/traits/war/che_돌격.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_돌격', traitModule) }; }, event_전투특기_무쌍: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_무쌍.js'), + import('../actionModules/traits/war/che_무쌍.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_무쌍', traitModule) }; }, event_전투특기_반계: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_반계.js'), + import('../actionModules/traits/war/che_반계.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_반계', traitModule) }; }, event_전투특기_보병: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_보병.js'), + import('../actionModules/traits/war/che_보병.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_보병', traitModule) }; }, event_전투특기_신산: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_신산.js'), + import('../actionModules/traits/war/che_신산.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_신산', traitModule) }; }, event_전투특기_신중: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_신중.js'), + import('../actionModules/traits/war/che_신중.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_신중', traitModule) }; }, event_전투특기_위압: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_위압.js'), + import('../actionModules/traits/war/che_위압.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_위압', traitModule) }; }, event_전투특기_의술: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_의술.js'), + import('../actionModules/traits/war/che_의술.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_의술', traitModule) }; }, event_전투특기_저격: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_저격.js'), + import('../actionModules/traits/war/che_저격.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_저격', traitModule) }; }, event_전투특기_집중: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_집중.js'), + import('../actionModules/traits/war/che_집중.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_집중', traitModule) }; }, event_전투특기_징병: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_징병.js'), + import('../actionModules/traits/war/che_징병.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_징병', traitModule) }; }, event_전투특기_척사: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_척사.js'), + import('../actionModules/traits/war/che_척사.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_척사', traitModule) }; }, event_전투특기_필살: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_필살.js'), + import('../actionModules/traits/war/che_필살.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_필살', traitModule) }; }, event_전투특기_환술: async () => { const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([ - import('../triggers/special/war/che_환술.js'), + import('../actionModules/traits/war/che_환술.js'), import('./eventBattleTrait.js'), ]); return { itemModule: createEventBattleTraitItemModule('event_전투특기_환술', traitModule) }; @@ -521,9 +526,7 @@ export const createItemModuleRegistry = implements GeneralActionModule { +class ItemGeneralActionRouter { constructor(private readonly registry: ItemModuleRegistry) {} private resolveModules(context: GeneralActionContext): Array> { @@ -631,38 +634,21 @@ class ItemGeneralActionRouter< return current; } - onArbitraryAction( - context: GeneralActionContext, - actionType: TriggerActionType, - phase?: TriggerActionPhase | null, - aux?: Record | null - ): Record | null { - let current = aux ?? null; + handleEvent( + context: GeneralActionEventContext, + event: GeneralActionEvent + ): GeneralActionEvent { + let current = event; for (const module of this.resolveModules(context)) { - if (!module.onArbitraryAction) { - // Declarative consumption rules are evaluated below even when - // the item has no custom arbitrary-action hook. - } else { - const result = module.onArbitraryAction(context, actionType, phase, current); - if (result !== undefined) { - current = result; - } - } + current = dispatchGeneralActionEventHandlers(module.eventHandlers, context, current); - const shouldConsume = module.consumeOn?.some( - (rule) => - rule.actionType === actionType && - (rule.phase === undefined || rule.phase === phase) && - (rule.command === undefined || current?.['command'] === rule.command) && - (!rule.successOnly || current?.['success'] === true) - ); - if (shouldConsume) { + if (current.type === 'strategy.succeeded' && module.consumeOnStrategySuccess) { const removed = removeEquippedItem(context.general, module.slot); if (removed?.itemKey === module.key) { - const consumedItems = Array.isArray(current?.['consumedItems']) - ? (current['consumedItems'] as unknown[]) - : []; - current = { ...(current ?? {}), consumedItems: [...consumedItems, module.key] }; + const strategyEvent = current as GeneralActionEvent<'strategy.succeeded', TriggerState>; + current = createGeneralActionEvent('strategy.succeeded', { + consumedItems: [...strategyEvent.payload.consumedItems, module.key], + }) as GeneralActionEvent; } } } @@ -762,7 +748,7 @@ class ItemWarActionRouter< export const createItemActionModules = ( registry: ItemModuleRegistry -): { general: GeneralActionModule[]; war: WarActionModule[] } => ({ +): { general: ReadonlyArray>; war: WarActionModule[] } => ({ general: [new ItemGeneralActionRouter(registry)], war: [new ItemWarActionRouter(registry)], }); diff --git a/packages/logic/src/items/types.ts b/packages/logic/src/items/types.ts index 184fed6..2c21350 100644 --- a/packages/logic/src/items/types.ts +++ b/packages/logic/src/items/types.ts @@ -2,28 +2,20 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { GeneralActionContext, GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.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 { GeneralActionEventHandlers } from '@sammo-ts/logic/actionModules/events.js'; import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; import type { WarUnit } from '@sammo-ts/logic/war/units.js'; export type ItemSlot = 'horse' | 'weapon' | 'book' | 'item'; -export interface ItemActionConsumptionRule { - actionType: TriggerActionType; - phase?: TriggerActionPhase | null; - command?: string; - successOnly?: boolean; -} - export interface ItemModule { key: string; name: string; @@ -34,7 +26,7 @@ export interface ItemModule, - actionType: TriggerActionType, - phase?: TriggerActionPhase | null, - aux?: Record | null - ): Record | null; + eventHandlers?: GeneralActionEventHandlers; getBattleInitTriggerList?(context: WarActionContext): WarTriggerCaller | null; diff --git a/packages/logic/src/triggers/actionModuleBundle.ts b/packages/logic/src/triggers/actionModuleBundle.ts deleted file mode 100644 index c46449c..0000000 --- a/packages/logic/src/triggers/actionModuleBundle.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; -import { compileCrewTypeCatalog } from '@sammo-ts/logic/crewType/catalog.js'; -import { createCrewTypeWarTriggerRegistry } from '@sammo-ts/logic/war/crewTypeTriggers.js'; -import { createInheritBuffModules } from '@sammo-ts/logic/inheritance/inheritBuff.js'; -import { - createItemActionModules, - createItemModuleRegistry, - ITEM_KEYS, - loadItemModules, - type ItemModule, -} from '@sammo-ts/logic/items/index.js'; -import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js'; -import type { GeneralActionModule } from './general-action.js'; -import type { WarActionModule } from '@sammo-ts/logic/war/actions.js'; -import { createOfficerLevelActionModules } from './officerLevel.js'; -import { - createTraitModuleRegistry, - DOMESTIC_TRAIT_KEYS, - loadDomesticTraitModules, - loadNationTraitModules, - loadPersonalityTraitModules, - loadWarTraitModules, - NATION_TRAIT_KEYS, - PERSONALITY_TRAIT_KEYS, - TraitGeneralActionRouter, - TraitWarActionRouter, - WAR_TRAIT_KEYS, -} from './special/index.js'; -import type { NationTraitModule } from './special/nation/index.js'; - -export interface ActionModuleBundle { - general: GeneralActionModule[]; - war: WarActionModule[]; - itemModules: ItemModule[]; - nationTraitModules: NationTraitModule[]; -} - -// General::getActionList와 같은 소유권 순서로 실제 턴과 시뮬레이터의 모듈을 조립한다. -export const loadActionModuleBundle = async ( - unitSet?: UnitSetDefinition -): Promise> => { - 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[]>, - ]); - const traitRegistry = createTraitModuleRegistry({ domestic, war, personality, nation }); - const officer = createOfficerLevelActionModules(); - const items = createItemActionModules(createItemModuleRegistry(itemModules)); - const inherit = createInheritBuffModules(); - const crewTypeCatalog = unitSet?.crewTypes?.length - ? compileCrewTypeCatalog(unitSet, createCrewTypeWarTriggerRegistry()) - : null; - - return { - general: [ - new TraitGeneralActionRouter('nation', traitRegistry), - officer.general, - new TraitGeneralActionRouter('domestic', traitRegistry), - new TraitGeneralActionRouter('war', traitRegistry), - new TraitGeneralActionRouter('personality', traitRegistry), - ...(crewTypeCatalog ? [crewTypeCatalog.generalActionModule] : []), - inherit.general as GeneralActionModule, - ...items.general, - ], - war: [ - new TraitWarActionRouter('nation', traitRegistry), - officer.war, - new TraitWarActionRouter('domestic', traitRegistry), - new TraitWarActionRouter('war', traitRegistry), - new TraitWarActionRouter('personality', traitRegistry), - ...(crewTypeCatalog ? [crewTypeCatalog.warActionModule] : []), - inherit.war as WarActionModule, - ...items.war, - ], - itemModules, - nationTraitModules: nation, - }; -}; diff --git a/packages/logic/src/triggers/general.ts b/packages/logic/src/triggers/general.ts index c69dd81..9f011ad 100644 --- a/packages/logic/src/triggers/general.ts +++ b/packages/logic/src/triggers/general.ts @@ -36,6 +36,11 @@ export interface GeneralActionContext; log?: GeneralActionLogSink; rng?: RandomGenerator; + time?: { + year: number; + month: number; + startYear: number; + }; } export interface GeneralTriggerContext< diff --git a/packages/logic/src/triggers/index.ts b/packages/logic/src/triggers/index.ts index 9f734e0..e3ce6fa 100644 --- a/packages/logic/src/triggers/index.ts +++ b/packages/logic/src/triggers/index.ts @@ -1,7 +1,2 @@ export * from './core.js'; export * from './general.js'; -export * from './general-action.js'; -export * from './types.js'; -export * from './officerLevel.js'; -export * from './actionModuleBundle.js'; -export * from './special/index.js'; diff --git a/packages/logic/src/war/actions.ts b/packages/logic/src/war/actions.ts index 996537a..e452cb9 100644 --- a/packages/logic/src/war/actions.ts +++ b/packages/logic/src/war/actions.ts @@ -2,7 +2,7 @@ import type { RandUtil } from '@sammo-ts/common'; import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js'; import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js'; -import type { WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarStatName } from '@sammo-ts/logic/actionModules/types.js'; import type { WarUnit } from './units.js'; import { WarTriggerCaller } from './triggers.js'; @@ -54,7 +54,7 @@ export class WarActionPipeline[]; - constructor(modules: Array | null | undefined>) { + constructor(modules: ReadonlyArray | null | undefined>) { this.modules = modules.filter(Boolean) as WarActionModule[]; } diff --git a/packages/logic/src/war/aftermath.ts b/packages/logic/src/war/aftermath.ts index 1e1e772..37d80bd 100644 --- a/packages/logic/src/war/aftermath.ts +++ b/packages/logic/src/war/aftermath.ts @@ -1,8 +1,10 @@ import { JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common'; import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js'; +import { createGeneralActionEvent } from '@sammo-ts/logic/actionModules/events.js'; +import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js'; import { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js'; -import { LogFormat, type LogEntryDraft } from '@sammo-ts/logic/logging/types.js'; +import { LogCategory, LogFormat, LogScope, type LogEntryDraft } from '@sammo-ts/logic/logging/types.js'; import { buildCrewTypeIndex, getTechCost, getTechLevel } from '@sammo-ts/logic/world/unitSet.js'; import type { WarUnitReport } from './types.js'; import type { @@ -530,6 +532,49 @@ export const resolveWarAftermath = + general.nationId !== 0 && + general.nationId === input.defenderCity.nationId && + general.cityId === input.defenderCity.id + ); + for (const general of cityDefenders) { + pipeline.dispatch( + { + general, + nation: input.defenderNation, + rng, + worldView: { + listGenerals: () => input.generals, + listGeneralsByCity: (cityId) => + input.generals.filter((candidate) => candidate.cityId === cityId), + listNations: () => input.nations, + }, + log: { + push: (text) => + logs.push({ + scope: LogScope.GENERAL, + category: LogCategory.ACTION, + format: LogFormat.MONTH, + generalId: general.id, + nationId: general.nationId, + text, + }), + }, + }, + createGeneralActionEvent('city.conquered', { + attacker: input.battle.attacker, + }) + ); + affectedGenerals.add(general); + } + conquest = resolveConquerCity(input, rng); logs.push(...conquest.logs); diff --git a/packages/logic/src/war/types.ts b/packages/logic/src/war/types.ts index 43c9542..f826a93 100644 --- a/packages/logic/src/war/types.ts +++ b/packages/logic/src/war/types.ts @@ -1,6 +1,7 @@ import type { RandUtil } from '@sammo-ts/common'; import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js'; +import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js'; import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js'; import type { LogEntryDraft } from '@sammo-ts/logic/logging/types.js'; import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js'; @@ -39,7 +40,7 @@ export interface WarGeneralInput | null | undefined>; + modules?: ReadonlyArray | null | undefined>; } export interface WarBattleInput { @@ -177,6 +178,7 @@ export interface WarAftermathInput | null | undefined>; calcNationTechGain?: (context: WarAftermathTechContext) => number; } diff --git a/packages/logic/src/war/units/general.ts b/packages/logic/src/war/units/general.ts index bdbbb4d..43098cc 100644 --- a/packages/logic/src/war/units/general.ts +++ b/packages/logic/src/war/units/general.ts @@ -10,7 +10,7 @@ import type { } from '@sammo-ts/logic/domain/entities.js'; import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js'; import { LogFormat } from '@sammo-ts/logic/logging/types.js'; -import type { WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarStatName } from '@sammo-ts/logic/actionModules/types.js'; import { getTechAbility, getTechCost } from '@sammo-ts/logic/world/unitSet.js'; import type { WarActionPipeline, WarActionContext } from '../actions.js'; import type { WarEngineConfig } from '../types.js'; diff --git a/packages/logic/test/actionModuleEvents.test.ts b/packages/logic/test/actionModuleEvents.test.ts new file mode 100644 index 0000000..2171228 --- /dev/null +++ b/packages/logic/test/actionModuleEvents.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest'; + +import type { General } from '../src/domain/entities.js'; +import { createGeneralActionEvent, type GeneralActionEvent } from '../src/actionModules/events.js'; +import { GeneralActionPipeline, type GeneralActionModule } from '../src/actionModules/general.js'; +import { createRefOrderedActionStack } from '../src/actionModules/bundle.js'; + +const makeGeneral = (): General => ({ + id: 1, + name: '이벤트 장수', + nationId: 1, + cityId: 1, + troopId: 0, + stats: { leadership: 70, strength: 70, intelligence: 70 }, + experience: 0, + dedication: 0, + officerLevel: 1, + role: { + personality: null, + specialDomestic: null, + specialWar: null, + items: { horse: null, weapon: null, book: null, item: null }, + }, + injury: 0, + gold: 1000, + rice: 1000, + crew: 1000, + crewTypeId: 1100, + train: 100, + atmos: 100, + age: 30, + npcState: 0, + triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, + meta: { killturn: 24 }, +}); + +describe('typed general action events', () => { + it('folds synchronous event handlers in the supplied ref ownership order', () => { + const trace: string[] = []; + const names = ['nation', 'officer', 'domestic', 'war', 'personality', 'crew', 'inherit', 'item']; + const modules: GeneralActionModule[] = names.map((name) => ({ + eventHandlers: { + 'strategy.succeeded': (_context, event) => { + trace.push(name); + return createGeneralActionEvent('strategy.succeeded', { + consumedItems: [...event.payload.consumedItems, name], + }); + }, + }, + })); + const stack = createRefOrderedActionStack({ + nation: modules[0]!, + officer: modules[1]!, + domestic: modules[2]!, + war: modules[3]!, + personality: modules[4]!, + crewType: modules[5]!, + inheritance: modules[6]!, + scenario: null, + items: [modules[7]!], + }); + const pipeline = new GeneralActionPipeline(stack); + + const result = pipeline.dispatch( + { general: makeGeneral() }, + createGeneralActionEvent('strategy.succeeded', { consumedItems: [] }) + ); + + expect(trace).toEqual(names); + expect(result.payload.consumedItems).toEqual(names); + }); +}); + +// tsc가 실행될 때만 평가하는 negative type contract입니다. +const assertCompileTimeContracts = (pipeline: GeneralActionPipeline, general: General): void => { + // @ts-expect-error item.purchased에는 slot이 필수입니다. + createGeneralActionEvent('item.purchased', { itemKey: 'x' }); + + const sold = createGeneralActionEvent('item.sold', { + itemKey: 'x', + slot: 'item', + }); + // @ts-expect-error item.sold dispatch에는 RNG와 time context가 모두 필수입니다. + pipeline.dispatch({ general }, sold); + + // @ts-expect-error 닫힌 protocol에 임의 event key를 추가할 수 없습니다. + createGeneralActionEvent('strategy.failed', { consumedItems: [] }); + + // @ts-expect-error unique-symbol shadow brand가 없는 event를 직접 위조할 수 없습니다. + const forged: GeneralActionEvent<'strategy.succeeded'> = { + type: 'strategy.succeeded', + payload: { consumedItems: [] }, + }; + void forged; + + const legacyEscapeHatch = { + // @ts-expect-error onArbitraryAction은 action module capability가 아닙니다. + onArbitraryAction: () => null, + } satisfies GeneralActionModule; + void legacyEscapeHatch; + + // @ts-expect-error leaf handler와 composite router를 한 module에 함께 선언할 수 없습니다. + const doubleEventPath: GeneralActionModule = { + eventHandlers: { + 'strategy.succeeded': (_context, event) => event, + }, + handleEvent: (_context, event) => event, + }; + void doubleEventPath; + + // @ts-expect-error 표준 stack은 officer slot을 생략할 수 없습니다. + createRefOrderedActionStack({ + nation: {}, + domestic: {}, + war: {}, + personality: {}, + crewType: null, + inheritance: {}, + scenario: null, + items: [], + }); +}; +void assertCompileTimeContracts; diff --git a/packages/logic/test/inheritBuff.test.ts b/packages/logic/test/inheritBuff.test.ts index ce6fb69..895efc1 100644 --- a/packages/logic/test/inheritBuff.test.ts +++ b/packages/logic/test/inheritBuff.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { General } from '../src/domain/entities.js'; import { createInheritBuffModules } from '../src/inheritance/inheritBuff.js'; -import { GeneralActionPipeline } from '../src/triggers/general-action.js'; +import { GeneralActionPipeline } from '../src/actionModules/general.js'; const buildGeneral = (inheritBuff: Record): General => ({ id: 1, diff --git a/packages/logic/test/itemActionEvents.test.ts b/packages/logic/test/itemActionEvents.test.ts new file mode 100644 index 0000000..f03acd6 --- /dev/null +++ b/packages/logic/test/itemActionEvents.test.ts @@ -0,0 +1,271 @@ +import { describe, expect, it } from 'vitest'; + +import type { RandomGenerator } from '@sammo-ts/common'; + +import type { General, Nation } from '../src/domain/entities.js'; +import type { TurnCommandEnv, TurnCommandItemCatalogEntry } from '../src/actions/turn/commandEnv.js'; +import { ActionDefinition as TradeItemAction } from '../src/actions/turn/general/che_장비매매.js'; +import { consumeSuccessfulStrategyItem } from '../src/actions/turn/general/strategyItemConsumption.js'; +import { GeneralActionPipeline } from '../src/actionModules/general.js'; +import { createRefOrderedActionStack } from '../src/actionModules/bundle.js'; +import { createItemActionModules, createItemModuleRegistry } from '../src/items/index.js'; +import { getEquippedItemInstance } from '../src/items/inventory.js'; +import { itemModule as dogiModule } from '../src/items/che_보물_도기.js'; +import { itemModule as strategyItemModule } from '../src/items/che_계략_이추.js'; +import { LogFormat } from '../src/logging/types.js'; + +const BASE_ENV: TurnCommandEnv = { + develCost: 100, + trainDelta: 35, + atmosDelta: 35, + maxTrainByCommand: 100, + maxAtmosByCommand: 100, + sabotageDefaultProb: 0.5, + sabotageProbCoefByStat: 0.1, + sabotageDefenceCoefByGeneralCount: 0.1, + sabotageDamageMin: 10, + sabotageDamageMax: 30, + openingPartYear: 200, + maxGeneral: 10, + defaultNpcGold: 1000, + defaultNpcRice: 1000, + defaultCrewTypeId: 1, + defaultSpecialDomestic: null, + defaultSpecialWar: null, + initialNationGenLimit: 10, + maxTechLevel: 10, + baseGold: 1000, + baseRice: 1000, + maxResourceActionAmount: 10000, +}; + +const makeGeneral = (itemKey: string | null): General => ({ + id: 1, + name: '도기 장수', + nationId: 1, + cityId: 1, + troopId: 0, + stats: { leadership: 70, strength: 70, intelligence: 70 }, + experience: 0, + dedication: 0, + officerLevel: 1, + role: { + personality: null, + specialDomestic: null, + specialWar: null, + items: { horse: null, weapon: null, book: null, item: itemKey }, + }, + injury: 0, + gold: 1000, + rice: 1000, + crew: 1000, + crewTypeId: 1100, + train: 100, + atmos: 100, + age: 30, + npcState: 0, + triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, + meta: { killturn: 24 }, +}); + +const makeNation = (): Nation => ({ + id: 1, + name: '도기국', + color: '#000000', + capitalCityId: 1, + chiefGeneralId: 1, + gold: 500, + rice: 500, + power: 0, + level: 1, + typeCode: 'None', + meta: {}, +}); + +const makeChoiceRng = (index: 0 | 1): { rng: RandomGenerator; calls: Array<[number, number]> } => { + const calls: Array<[number, number]> = []; + return { + calls, + rng: { + nextFloat1: () => index, + nextBool: () => index === 1, + nextInt: (minInclusive, maxExclusive) => { + calls.push([minInclusive, maxExclusive]); + return index; + }, + }, + }; +}; + +const dogiCatalog: Record = { + che_보물_도기: { + slot: 'item', + name: '도기(보물)', + rawName: '도기', + cost: 200, + reqSecu: 0, + buyable: false, + unique: true, + }, +}; + +const createItemOnlyStack = (items: ReturnType['general']) => { + const noOp = {}; + return createRefOrderedActionStack({ + nation: noOp, + officer: noOp, + domestic: noOp, + war: noOp, + personality: noOp, + crewType: null, + inheritance: noOp, + scenario: null, + items, + }); +}; + +describe('typed item lifecycle events', () => { + it.each([ + { + name: 'bit 0은 ref choice index 0인 금을 선택한다', + bit: 0 as const, + year: 200, + expectedGeneral: { gold: 6100, rice: 1000 }, + expectedNation: { gold: 5500, rice: 500 }, + resource: '금', + }, + { + name: 'bit 1은 ref choice index 1인 쌀을 선택한다', + bit: 1 as const, + year: 200, + expectedGeneral: { gold: 1100, rice: 6000 }, + expectedNation: { gold: 500, rice: 5500 }, + resource: '쌀', + }, + { + name: '2년 경계에서 보충량이 15,000으로 증가한다', + bit: 0 as const, + year: 202, + expectedGeneral: { gold: 8600, rice: 1000 }, + expectedNation: { gold: 8000, rice: 500 }, + resource: '금', + }, + ])('$name', ({ bit, year, expectedGeneral, expectedNation, resource }) => { + const general = makeGeneral('che_보물_도기'); + const nation = makeNation(); + const logs: Array<{ message: string; format: LogFormat | undefined }> = []; + let stateAtSaleLog: { gold: number; equipped: string | null } | null = null; + const { rng, calls } = makeChoiceRng(bit); + const itemModules = createItemActionModules(createItemModuleRegistry([dogiModule])); + const action = new TradeItemAction({ + ...BASE_ENV, + itemCatalog: dogiCatalog, + generalActionModules: createItemOnlyStack(itemModules.general), + }); + + const outcome = action.resolve( + { + general, + nation, + rng, + time: { year, month: 1, startYear: 200 }, + addLog: (message, options) => { + if (logs.length === 0) { + stateAtSaleLog = { + gold: general.gold, + equipped: general.role.items.item, + }; + } + logs.push({ message, format: options?.format }); + }, + }, + { itemType: 'item', itemCode: 'None' } + ); + + expect({ gold: general.gold, rice: general.rice }).toEqual(expectedGeneral); + expect({ gold: nation.gold, rice: nation.rice }).toEqual(expectedNation); + expect(general.role.items.item).toBeNull(); + expect(calls).toEqual([[0, 2]]); + expect(stateAtSaleLog).toEqual({ gold: 1000, equipped: 'che_보물_도기' }); + expect(logs.slice(0, 2)).toEqual([ + { + message: '도기(보물)를 판매했습니다.', + format: LogFormat.MONTH, + }, + { + message: `재산과 국고에 총 ${resource} ${year === 202 ? '15,000' : '10,000'}을 보충합니다.`, + format: LogFormat.MONTH, + }, + ]); + expect(outcome.effects).toContainEqual( + expect.objectContaining({ + type: 'general:patch', + patch: expect.objectContaining(expectedGeneral), + }) + ); + expect(outcome.effects).toContainEqual( + expect.objectContaining({ + type: 'nation:patch', + targetId: nation.id, + patch: expect.objectContaining(expectedNation), + }) + ); + }); + + it.each([ + ['che_치료_환약', 3], + ['event_충차', 2], + ])('%s 구매 시 charge를 canonical inventory에 초기화한다', (itemKey, charges) => { + const catalog: Record = { + [itemKey]: { + slot: 'item', + name: itemKey, + rawName: itemKey, + cost: 100, + reqSecu: 0, + buyable: true, + unique: false, + initialCharges: charges, + }, + }; + const general = makeGeneral(null); + const action = new TradeItemAction({ + ...BASE_ENV, + itemCatalog: catalog, + }); + const { rng } = makeChoiceRng(0); + + action.resolve( + { + general, + nation: makeNation(), + rng, + time: { year: 200, month: 1, startYear: 200 }, + addLog: () => {}, + }, + { itemType: 'item', itemCode: itemKey } + ); + + expect(getEquippedItemInstance(general, 'item')).toMatchObject({ + itemKey, + state: { charges }, + }); + }); + + it('계략 성공 capability만 소비하며 typed 결과로 소비 item을 반환한다', () => { + const general = makeGeneral('che_계략_이추'); + const itemModules = createItemActionModules(createItemModuleRegistry([strategyItemModule])); + const pipeline = new GeneralActionPipeline(itemModules.general); + const { rng } = makeChoiceRng(0); + + const consumedItems = consumeSuccessfulStrategyItem(pipeline, { + general, + nation: makeNation(), + rng, + addLog: () => {}, + }); + + expect(consumedItems).toEqual(['che_계략_이추']); + expect(general.role.items.item).toBeNull(); + }); +}); diff --git a/packages/logic/test/scenarios/general_commands_new.test.ts b/packages/logic/test/scenarios/general_commands_new.test.ts index abf46e9..cb411dc 100644 --- a/packages/logic/test/scenarios/general_commands_new.test.ts +++ b/packages/logic/test/scenarios/general_commands_new.test.ts @@ -23,6 +23,7 @@ import { getEquippedItemInstance, loadItemModules, } from '../../src/items/index.js'; +import { createRefOrderedActionStack } from '../../src/actionModules/bundle.js'; describe('General Commands New Scenario', () => { // 1. Setup Environment @@ -449,7 +450,21 @@ describe('General Commands New Scenario', () => { expect(spyInfo['2']).toBe(3); // City 2 spied level 3 // 3. Destroy (G1 -> C2) - const strategyEnv = { ...systemEnv, generalActionModules: itemGeneralModules }; + const noOpModule = {}; + const strategyEnv: TurnCommandEnv = { + ...systemEnv, + generalActionModules: createRefOrderedActionStack({ + nation: noOpModule, + officer: noOpModule, + domestic: noOpModule, + war: noOpModule, + personality: noOpModule, + crewType: null, + inheritance: noOpModule, + scenario: null, + items: itemGeneralModules, + }), + }; const destroyDef = destroySpec.createDefinition(strategyEnv); await runner.runTurn([ { diff --git a/packages/logic/test/specialActions.test.ts b/packages/logic/test/specialActions.test.ts index 0200adc..23c5c36 100644 --- a/packages/logic/test/specialActions.test.ts +++ b/packages/logic/test/specialActions.test.ts @@ -5,14 +5,14 @@ import { ConstantRNG, RandUtil } from '@sammo-ts/common'; import type { City, General, Nation } from '../src/domain/entities.js'; import type { RandomGenerator } from '../src/index.js'; import type { UnitSetDefinition } from '../src/world/types.js'; -import { GeneralActionPipeline } from '../src/triggers/general-action.js'; +import { GeneralActionPipeline } from '../src/actionModules/general.js'; import { createGeneralTriggerContext } from '../src/triggers/general.js'; import { - createTraitModuleRegistry, + createTraitCatalog, createTraitModules, loadDomesticTraitModules, loadWarTraitModules, -} from '../src/triggers/special/index.js'; +} from '../src/actionModules/traits/index.js'; import { ActionLogger } from '../src/logging/actionLogger.js'; import { WarActionPipeline } from '../src/war/actions.js'; import { WarCrewType } from '../src/war/crewType.js'; @@ -174,7 +174,7 @@ describe('trait modules', () => { it('applies domestic and war modifiers in general pipeline', async () => { const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']); const war = await loadWarTraitModules(['che_의술', 'che_징병']); - const registry = createTraitModuleRegistry({ domestic, war }); + const registry = createTraitCatalog({ domestic, war }); const traitModules = createTraitModules(registry); const pipeline = new GeneralActionPipeline(traitModules.general); @@ -201,7 +201,7 @@ describe('trait modules', () => { it('heals city generals with 의술 pre-turn trigger', async () => { const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']); const war = await loadWarTraitModules(['che_의술', 'che_징병']); - const registry = createTraitModuleRegistry({ domestic, war }); + const registry = createTraitCatalog({ domestic, war }); const traitModules = createTraitModules(registry); const pipeline = new GeneralActionPipeline(traitModules.general); @@ -254,7 +254,7 @@ describe('trait modules', () => { it('activates 의술 battle trigger and reduces damage', async () => { const domestic = await loadDomesticTraitModules(['che_인덕', 'che_발명']); const war = await loadWarTraitModules(['che_의술', 'che_징병']); - const registry = createTraitModuleRegistry({ domestic, war }); + const registry = createTraitCatalog({ domestic, war }); const traitModules = createTraitModules(registry); const rng = new RandUtil(new ConstantRNG(0)); diff --git a/packages/logic/test/specialTraitSelectorParity.test.ts b/packages/logic/test/specialTraitSelectorParity.test.ts index 2faeff7..fa87b90 100644 --- a/packages/logic/test/specialTraitSelectorParity.test.ts +++ b/packages/logic/test/specialTraitSelectorParity.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; import { RandUtil, type RNG } from '@sammo-ts/common'; -import { TraitRequirement, TraitWeightType } from '../src/triggers/special/requirements.js'; -import { TraitSelector } from '../src/triggers/special/selector.js'; -import type { TraitModule } from '../src/triggers/special/types.js'; +import { TraitRequirement, TraitWeightType } from '../src/actionModules/traits/requirements.js'; +import { TraitSelector } from '../src/actionModules/traits/selector.js'; +import type { TraitModule } from '../src/actionModules/traits/types.js'; class ScriptedRng implements RNG { public floatCalls = 0; @@ -62,22 +62,12 @@ const trait = ( describe('legacy speciality selector parity', () => { it('sets positive and negative stat bits in the legacy order', () => { - expect( - TraitSelector.calcCondGeneric( - { leadership: 80, strength: 75, intelligence: 40 }, - scenarioStat - ) - ).toBe( - TraitRequirement.STAT_LEADERSHIP | - TraitRequirement.STAT_STRENGTH | - TraitRequirement.STAT_NOT_INTEL + expect(TraitSelector.calcCondGeneric({ leadership: 80, strength: 75, intelligence: 40 }, scenarioStat)).toBe( + TraitRequirement.STAT_LEADERSHIP | TraitRequirement.STAT_STRENGTH | TraitRequirement.STAT_NOT_INTEL + ); + expect(TraitSelector.calcCondGeneric({ leadership: 70, strength: 70, intelligence: 70 }, scenarioStat)).toBe( + TraitRequirement.STAT_STRENGTH ); - expect( - TraitSelector.calcCondGeneric( - { leadership: 70, strength: 70, intelligence: 70 }, - scenarioStat - ) - ).toBe(TraitRequirement.STAT_STRENGTH); }); it('rounds the dex threshold and still consumes a value choice when every dex is zero', () => { diff --git a/packages/logic/test/warAftermath.test.ts b/packages/logic/test/warAftermath.test.ts index 5336f3b..fcf5d3b 100644 --- a/packages/logic/test/warAftermath.test.ts +++ b/packages/logic/test/warAftermath.test.ts @@ -1,11 +1,13 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { ConstantRNG, RandUtil } from '@sammo-ts/common'; import type { City, General, Nation } from '../src/domain/entities.js'; +import type { GeneralActionModule } from '../src/actionModules/general.js'; import type { UnitSetDefinition } from '../src/world/types.js'; import { resolveWarAftermath } from '../src/war/aftermath.js'; import type { WarAftermathConfig } from '../src/war/types.js'; +import { LogFormat } from '../src/logging/types.js'; const buildUnitSet = (): UnitSetDefinition => ({ id: 'test', @@ -305,6 +307,88 @@ describe('war aftermath', () => { ); }); + it('dispatches city conquest to every stationed defender before collapse RNG', () => { + const rng = new RandUtil(new ConstantRNG(0)); + const draws = [0.01, 0.02, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]; + const nextFloat = vi.spyOn(rng, 'nextFloat1').mockImplementation(() => { + const value = draws.shift(); + if (value === undefined) { + throw new Error('unexpected RNG draw'); + } + return value; + }); + const attackerNation = buildNation(1); + const defenderNation = buildNation(2); + const attackerCity = buildCity(1, 1); + const defenderCity = buildCity(2, 2); + const attacker = buildGeneral(1, 1, 1); + const firstDefender = buildGeneral(2, 2, 2); + const secondDefender = buildGeneral(3, 2, 2); + secondDefender.crew = 0; + const elsewhere = buildGeneral(4, 2, 3); + const dispatchOrder: number[] = []; + const module: GeneralActionModule = { + eventHandlers: { + 'city.conquered': (context, event) => { + dispatchOrder.push(context.general.id); + context.general.triggerState.meta.conqueredBy = event.payload.attacker.id; + context.rng.nextFloat1(); + context.log?.push(`점령 이벤트 ${context.general.id}`); + }, + }, + }; + + const outcome = resolveWarAftermath({ + battle: { + attacker, + defenders: [firstDefender], + defenderCity, + logs: [], + conquered: true, + reports: [], + }, + attackerNation, + defenderNation, + attackerCity, + defenderCity, + nations: [attackerNation, defenderNation], + cities: [attackerCity, defenderCity], + generals: [attacker, firstDefender, secondDefender, elsewhere], + unitSet: buildUnitSet(), + config: buildConfig(), + time: { + year: 200, + month: 1, + startYear: 180, + }, + rng, + generalActionModules: [module], + }); + + expect(dispatchOrder).toEqual([firstDefender.id, secondDefender.id]); + expect(firstDefender.triggerState.meta.conqueredBy).toBe(attacker.id); + expect(secondDefender.triggerState.meta.conqueredBy).toBe(attacker.id); + expect(elsewhere.triggerState.meta.conqueredBy).toBeUndefined(); + // The first two draws belong to the two event handlers. Collapse then + // consumes the same stream: 0.2/0.3 for the first defender and + // 0.4/0.5 for the second. + expect(firstDefender.gold).toBe(740); + expect(firstDefender.rice).toBe(710); + expect(secondDefender.gold).toBe(680); + expect(secondDefender.rice).toBe(650); + expect(elsewhere.gold).toBe(620); + expect(elsewhere.rice).toBe(590); + expect(nextFloat).toHaveBeenCalledTimes(8); + expect(draws).toEqual([]); + expect(outcome.logs.filter((log) => log.text.startsWith('점령 이벤트')).map((log) => log.format)).toEqual([ + LogFormat.MONTH, + LogFormat.MONTH, + ]); + expect(outcome.generals.map((general) => general.id)).toEqual( + expect.arrayContaining([firstDefender.id, secondDefender.id]) + ); + }); + it('preserves the first contributor when conflict values are tied', () => { const attackerNation = buildNation(1); const defenderNation = buildNation(2); diff --git a/tools/integration-tests/test/monthlyDisasterCoreReference.integration.test.ts b/tools/integration-tests/test/monthlyDisasterCoreReference.integration.test.ts index d426eb8..177ecce 100644 --- a/tools/integration-tests/test/monthlyDisasterCoreReference.integration.test.ts +++ b/tools/integration-tests/test/monthlyDisasterCoreReference.integration.test.ts @@ -211,7 +211,7 @@ const projectReferenceLogs = (trace: ReferenceMonthlyTrace) => [ })), ]; -const buildCoreWorld = async (generalActionModules: GeneralActionModule[]): Promise => { +const buildCoreWorld = async (generalActionModules: ReadonlyArray): Promise => { const state: TurnWorldState = { id: 1, currentYear: 193, diff --git a/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts b/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts index 001ebd9..401403f 100644 --- a/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts +++ b/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts @@ -462,10 +462,7 @@ const generalActiveActionInheritanceCases: GeneralActiveActionInheritanceCase[] }, ]; -const readActiveActionPoints = ( - snapshot: { generals: Array> }, - generalId: number -): number => { +const readActiveActionPoints = (snapshot: { generals: Array> }, generalId: number): number => { const general = snapshot.generals.find((entry) => entry.id === generalId); const value = general?.inheritActiveActionPoints; return typeof value === 'number' && Number.isFinite(value) ? value : 0; @@ -1691,6 +1688,13 @@ const militaryPreparationBoundaryCases: MilitaryPreparationBoundaryCase[] = [ actorPatch: { itemWeapon: 'che_무기_11_고정도' }, completed: true, }, + { + name: 'equipment trade applies the dogi sale side effect before removing it', + action: 'che_장비매매', + args: { itemType: 'item', itemCode: 'None' }, + actorPatch: { itemExtra: 'che_보물_도기' }, + completed: true, + }, { name: 'disband rejects zero crew', action: 'che_소집해제', @@ -3524,7 +3528,12 @@ integration('general special reset constraint, state, unique lottery, and log pa referenceActor?.itemBook, referenceActor?.itemExtra, ]; - const coreItems = [coreActor?.itemHorse, coreActor?.itemWeapon, coreActor?.itemBook, coreActor?.itemExtra]; + const coreItems = [ + coreActor?.itemHorse, + coreActor?.itemWeapon, + coreActor?.itemBook, + coreActor?.itemExtra, + ]; expect(referenceItems.some((item) => item && item !== 'None')).toBe(true); expect(coreItems).toEqual(referenceItems); }