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

This commit is contained in:
2026-07-30 03:31:31 +00:00
parent 96b41ef4b4
commit 7d3cb50a5b
168 changed files with 1554 additions and 631 deletions
@@ -0,0 +1,84 @@
# 장수 행동 모듈 프로토콜
`packages/logic/src/actionModules/`는 ref의 `iAction`을 그대로 복사한 범용
interface가 아니라, 실제 core2026 실행 경계만 타입으로 표현합니다. 계산
hook, 우선순위 trigger, 의미 이벤트는 서로 다른 실행 계약입니다.
## 세 가지 실행 계약
| 계약 | core2026 경계 | 실행 의미 |
| ---------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
| 순차 계산 hook | `GeneralActionPipeline` | ref의 `General::getActionList()` 순서대로 값을 fold합니다. |
| 우선순위 trigger | `triggers/core.ts`, `triggers/general.ts`, 전투 trigger | priority, 삽입 순서와 unique ID 중복 제거를 보존한 뒤 fire합니다. |
| 의미 이벤트 | `actionModules/events.ts` | 닫힌 이벤트별 payload와 context를 동기적으로 순회합니다. |
priority trigger를 의미 이벤트로 바꾸거나, 의미 이벤트를 `TriggerCaller`
감싸지 않습니다. 두 경로는 정렬과 중복 제거 의미가 다릅니다.
## ref 순서와 소유권
정기턴은 `loadActionModuleBundle()`에서, 전투 시뮬레이터는 같은
`createRefOrderedActionStack()` factory에서 제품용 action stack을
조립합니다. 순서는 다음과 같습니다.
1. 국가 타입
2. 관직
3. 내정 특기
4. 전투 특기
5. 성격
6. 병종
7. 계승 버프
8. 시나리오 효과
9. 아이템
`RefOrderedActionStack`의 readonly unique-symbol brand는 임의 배열을 제품용
표준 stack으로 오인하지 않게 하는 shadow type입니다. 모든 slot을 명시하는
factory에서만 이 brand를 만들 수 있으며, 예약턴 runtime env에도 spread하지
않고 그대로 전달합니다. 현재 시나리오 효과 runtime module은 이식되지 않아
해당 slot은 명시적으로 `null`입니다. 따라서 이 brand는 순서와 slot 소유권을
증명하며, 시나리오 효과 구현 완료를 뜻하지 않습니다.
## 닫힌 의미 이벤트
`GeneralActionEventPayloadMap`이 허용하는 이벤트와 payload의 단일
source입니다. 현재 이벤트는 장비 구매·판매, 계략 성공, 도시 점령입니다.
- 이벤트는 `createGeneralActionEvent()`만 생성합니다. private
unique-symbol brand 때문에 객체 literal로 위조할 수 없습니다.
- `GeneralActionEventContext<K>`가 이벤트별 필수 능력을 정합니다. 예를 들어
판매는 RNG와 연월, 도시 점령은 RNG가 없으면 compile되지 않습니다.
- leaf module은 `eventHandlers`에 처리하는 이벤트 key만 선언합니다.
- trait, 병종과 item catalog 같은 합성 router만 내부 `handleEvent`
구현합니다. 두 capability는 `never`를 사용한 상호 배타적 union이라 한
module에서 동시에 선언할 수 없습니다.
- 새 문자열 action name, `phase`, `aux: Record<string, unknown>`를 범용
우회로로 추가하지 않습니다.
새 이벤트를 추가할 때는 payload map과 context 조건을 먼저 추가한 뒤,
실제 producer와 필요한 handler만 연결합니다. 존재하지 않는 handler
종류를 interface에 선행 추가하지 않습니다.
## 저장과 RNG 경계
의미 이벤트는 producer가 가진 객체를 동기적으로 수정합니다. producer는
이벤트 전후의 ref mutation 순서를 그대로 유지한 뒤 기존 effect/flush
경계에 결과를 전달합니다.
- 장비 판매는 판매 대금 반영 → 판매 이벤트 → 슬롯 제거 순서입니다.
- 도기 판매의 2분기는 ref `choice([gold, rice])`와 같이 index 0이 금,
index 1이 쌀입니다.
- 도시 점령은 점령 도시의 수비국 장수 전원을 입력 순서로 호출한 뒤 국가
멸망 손실을 계산합니다. 이벤트 handler와 멸망 손실은 같은
`ConquerCity` RNG 객체를 이어 씁니다.
- 계략 성공 아이템 소비는 `consumeOnStrategySuccess`라는 명시 capability로
선언하며 다른 임의 action 이름과 공유하지 않습니다.
## 검증 경계
`actionModuleEvents.test.ts`는 표준 순서, 이벤트 brand와 잘못된 context의
compile 실패를 검증합니다. `itemActionEvents.test.ts`는 도기 분기와
연차 경계, 충차·환약 초기 충전, 계략 성공 소비를 검증합니다.
`warAftermath.test.ts`는 도시 점령 대상과 공유 RNG 소비 순서를 검증합니다.
ref↔core 실제 명령 차등은
`turnCommandGeneralMatrix.integration.test.ts`의 도기 판매 fixture가
담당합니다.
@@ -200,6 +200,22 @@ non-item versions (see `ActionItem/che_저격_매화수전.php`).
per-phase activation.
- Ensure RNG usage stays deterministic (`RandUtil` everywhere in triggers).
## Current core2026 boundary
core2026 does not expose the whole legacy `iAction` surface as one interface.
Modifier folds and semantic side effects live under `src/actionModules/`;
priority callers remain under `src/triggers/` and the war trigger modules.
This separation is required because `TriggerCaller` sorts by priority and
deduplicates unique IDs, while semantic events synchronously preserve module
order and producer-owned mutation/RNG boundaries.
The former copied `onArbitraryAction(actionName, phase, aux)` surface was
removed. Equipment purchase/sale, strategy success and city conquest now use
the closed, nominally branded protocol documented in
[장수 행동 모듈 프로토콜](./action-module-protocol.md). This is a core2026
typing boundary only; the ref call order and persisted effects remain the
compatibility contract.
## Related Files
- `legacy/hwe/sammo/iAction.php`
+6 -6
View File
@@ -3,12 +3,12 @@
이 핸드북은 새 기능을 어디에 넣을지뿐 아니라 요청이 어떤 경계를 지나 상태로 남는지 설명합니다. 먼저
[문서 기준선](../reference-baseline.md)을 확인하고, 변경 성격에 따라 다음 순서로 읽어 주세요.
| 변경하려는 것 | 먼저 읽을 문서 | 주로 확인할 코드 |
| -------------------------- | ---------------------------------------------------- | -------------------------------------------- |
| 화면·라우팅·조회 API | [시스템 아키텍처](./system-architecture.md) | `app/*-frontend`, `app/*-api` |
| 턴 입력·게임 상태 mutation | [요청·턴·저장 흐름](./request-turn-persistence.md) | `app/game-api`, `app/game-engine` |
| 명령·전투·월간 로직 | [도메인 로직과 핵심 클래스](./domain-and-classes.md) | `packages/logic`, `app/game-engine/src/turn` |
| 새 파일 위치·검증 범위 | [파일 지도와 변경 절차](./code-map.md) | package manifest, test, docs |
| 변경하려는 것 | 먼저 읽을 문서 | 주로 확인할 코드 |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| 화면·라우팅·조회 API | [시스템 아키텍처](./system-architecture.md) | `app/*-frontend`, `app/*-api` |
| 턴 입력·게임 상태 mutation | [요청·턴·저장 흐름](./request-turn-persistence.md) | `app/game-api`, `app/game-engine` |
| 명령·전투·월간 로직 | [도메인 로직과 핵심 클래스](./domain-and-classes.md), [장수 행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `packages/logic`, `app/game-engine/src/turn` |
| 새 파일 위치·검증 범위 | [파일 지도와 변경 절차](./code-map.md) | package manifest, test, docs |
## 읽을 때 지켜야 할 경계