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