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

4.8 KiB
Raw Blame History

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