# 분기 맵 노드 진행 (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). 절차적 맵·무작위 분기·연결선 그리기. 새 카드.