Files
maplecontest/docs/superpowers/specs/2026-06-09-map-nodes-design.md
2026-06-09 03:18:49 +09:00

83 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 분기 맵 노드 진행 (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). 절차적 맵·무작위 분기·연결선 그리기. 새 카드.