docs(E3): 분기 맵 노드 설계·구현 계획

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 03:18:49 +09:00
parent 15975d7f51
commit 444d02367e
2 changed files with 575 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
# 분기 맵 노드 진행 (TODO E3) — 설계
> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO E 분해(E3) + SlayDeckController/run-loop-core 분석.
> 선행: E1+E2(런 루프 코어) 완료. 후속: E4(상점/휴식)·E5(유물)·E6(보스 연출/저장).
## 문제
E1+E2는 보상 후 자동으로 다음 전투로 넘어간다(고정 N). 로그라이크는 **플레이어가 맵에서 경로를
선택**해야 한다. 분기 노드 맵과 노드별 적 차등이 필요하다.
## 범위
플레이어가 **분기 맵(작성된 DAG)** 에서 다음 노드를 선택 → 노드 타입(전투/엘리트/보스)대로 전투
(적은 데이터로 차등) → 보상 → 맵으로 복귀 → 보스 클리어 시 "런 클리어". **상점/휴식·유물·저장·
절차적 생성·연결선 그리기는 범위 밖**. 맵 스키마는 상점/휴식 타입을 미래 수용.
## 설계
### 데이터
**`data/map.json`** (분기 DAG):
```json
{
"start": ["A", "B"],
"nodes": {
"A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] },
"B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["D", "E"] },
"C": { "type": "elite", "enemy": "slime_elite", "row": 2, "col": -2, "next": ["BOSS"] },
"D": { "type": "combat", "enemy": "slime", "row": 2, "col": 0, "next": ["BOSS"] },
"E": { "type": "combat", "enemy": "slime", "row": 2, "col": 2, "next": ["BOSS"] },
"BOSS": { "type": "boss", "enemy": "slime_boss", "row": 3, "col": 0, "next": [] }
}
}
```
- `type` ∈ {combat, elite, boss} (이 슬라이스). `enemy`는 enemies.json id. `row`(1=시작), `col`(레이아웃 x 단위), `next`(도달 노드 ids).
**`data/enemies.json`** 확장:
```json
"slime_elite": { "name": "정예 슬라임", "maxHp": 70,
"intents": [ {Attack 14}, {Attack 8}, {Defend 10} ] },
"slime_boss": { "name": "슬라임 킹", "maxHp": 120,
"intents": [ {Attack 18}, {Defend 12}, {Attack 10}, {Attack 22} ] }
```
(`activeEnemy`는 유지하되 런은 맵 노드의 enemy로 전투. F 시뮬레이터는 여전히 activeEnemy 기준 — 맵 적 시뮬은 후속.)
### 상태 (SlayDeckController 속성 추가)
- `Enemies`(any) — 전체 적 테이블(id→정의). 생성기가 enemies.json 전체 주입.
- `MapNodes`(any) — 그래프(id→{type, enemy, row, col, next}).
- `MapStart`(any) — 1행 노드 id 배열.
- `CurrentNodeId`(string) — 현재 위치("" = 시작 전).
- `CurrentEnemyId`(string) — 현재 전투 적 id.
### 메서드
- `StartRun`(수정): 런 상태 초기화 + `Enemies`/`MapNodes`/`MapStart` 세팅 + `CurrentNodeId=""` +
BindButtons(맵 노드 버튼 포함, 1회) → `self:ShowMap()` (기존 StartCombat 대신).
- `ShowMap`(신규): 선택 가능 노드 결정(CurrentNodeId=="" 면 MapStart, 아니면 MapNodes[CurrentNodeId].next).
각 노드 버튼 활성/비활성·라벨 갱신, 전투 UI 가리고 MapHud 표시(Enable).
- `IsReachable(id)`(헬퍼) — 현재 선택 가능 목록에 id 포함 여부.
- `PickNode(id)`(신규): `IsReachable(id)` 아니면 무시. `CurrentNodeId=id`,
`CurrentEnemyId=MapNodes[id].enemy`, MapHud 숨김 → `StartCombat()`.
- `StartCombat`(수정): 적을 `self.Enemies[self.CurrentEnemyId]`에서 로드(이름/HP/의도). Floor 증가 로직 제거.
- `CheckCombatEnd`(수정): 승리 시 골드+15 → 현재 노드 `type=="boss"``ShowResult("런 클리어!")`+RunActive=false;
아니면 `OfferReward`. 패배 → "패배..."+RunActive=false.
- `PickReward`(수정): 카드 처리 후 `StartCombat` 대신 `self:ShowMap()`.
### UI (MapHud, 신규)
- 평소 숨김. 풀스크린 모달 배경 + 제목 "다음 노드 선택".
- 노드 버튼 6개: 위치 = (col×스페이싱, 화면중앙+row×행간), 라벨(전투/엘리트/보스 + 적 이름).
- 선택 가능 노드만 밝게·클릭, 나머지 어둡게(반투명). 클릭 → `PickNode(id)`.
- 연결선은 생략(도달성=활성/비활성으로 표현; 연결선 그리기는 후속 폴리시).
### 단일 소스
모든 변경은 `tools/gen-slaydeck.mjs`에서 생성. map.json/enemies.json은 데이터 단일 소스.
## 검증 (메이커 Play)
- StartRun → MapHud, 1행 A·B만 선택 가능(나머지 비활성).
- A 선택 → 슬라임 전투 → 승리 → 보상 → 맵 복귀, 이제 C·D 선택 가능(B쪽 E는 불가).
- 엘리트 노드 → 정예 슬라임(HP 70) 전투. 보스 노드 → 슬라임 킹(HP 120).
- 보스 승리 → "런 클리어!". 패배 → "패배...". 도달 불가 노드 클릭 → 무시.
- 생성기 결정적, JSON 유효. (버튼 클릭은 런타임 — MCP는 PickNode/PlayCard/PickReward 직접 호출로 검증.)
## 범위 밖 (금지)
- 상점/휴식 노드 동작(E4)·유물(E5)·저장(E6). 절차적 맵·무작위 분기·연결선 그리기. 새 카드.