From d1e51878c3158a414d7c38291c20a516286db893 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 15 Jun 2026 14:21:04 +0900 Subject: [PATCH] =?UTF-8?q?docs(node-map):=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EB=A7=B5=20UI=20=EA=B0=95=ED=99=94=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-15-node-map-ui.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-15-node-map-ui.md diff --git a/docs/superpowers/plans/2026-06-15-node-map-ui.md b/docs/superpowers/plans/2026-06-15-node-map-ui.md new file mode 100644 index 0000000..53e6ec8 --- /dev/null +++ b/docs/superpowers/plans/2026-06-15-node-map-ui.md @@ -0,0 +1,227 @@ +# 노드 맵 UI 강화 구현 계획 + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. 설계: `docs/superpowers/specs/2026-06-15-node-map-ui-design.md`. 산출물(`ui/DefaultGroup.ui`·`*.codeblock`)은 Read/Edit 금지 — `tools/deck/gen-slaydeck.mjs` 소스·`data/*.json`만 수정 후 재생성. 검증은 `node tools/verify/count.mjs`(카운트)와 메이커 플레이테스트. + +**Goal:** 맵 노드 선택 화면(MapHud)을 단색 박스+텍스트 → 공식 메이플 아이콘 노드 + 배경 이미지로 강화하고, 아이콘/배경 RUID를 `data/nodeicons.json`로 외부화해 교체를 쉽게 한다. + +**Architecture:** 단일 소스(`data/nodeicons.json` + `tools/deck/gen-slaydeck.mjs`) → 산출물 재생성. 노드 = 아이콘 스프라이트(타입별 ImageRUID 런타임 주입, 상태는 Color 틴트), 배경 = MapHud 루트 이미지 + 반투명 오버레이. 절차 랜덤 배치·간선·버튼 바인딩 불변. + +**Tech Stack:** Node.js ESM 생성기, MSW Lua(codeblock). + +**확정 RUID** (공식 maplestory, 썸네일 검수): combat=`f98db6823e894a4f90308d61f75894ac`, elite=`793ed8a757534b89a82f460747d2df24`, boss=`423056cdbbc04f4da131b9721c404d96`, shop=`da37e1fac55d455b9ade08569f09f798`, rest=`b86c1b0568bd45f3ae4a4b97e1b4a594`, treasure=`f8a6d58e20f54e2ca899485055df1ce4`, background=`d84241f17de344a097f5b96ac914f1d2`. + +**현재 코드 기준선**(gen-slaydeck.mjs): MapHud emit `1662~1763`(루트 `1664`, pushMapNode `1696`, 그리드 `1727`, 도트 displayOrder 1), RenderMapNode `5615~5677`, luaFramesTable `72`, OnBeginPlay 주입 `2906`, StartRun 주입 `3361`, CardFrames prop `2854`, CHEST 상수 `84`, sprite 헬퍼 `297`(dataId→ImageRUID, type 0=이미지). + +--- + +### Task 1: `data/nodeicons.json` + 생성기 로드·검증·직렬화 + +**Files:** Create `data/nodeicons.json` · Modify `tools/deck/gen-slaydeck.mjs` + +- [ ] **Step 1:** `data/nodeicons.json` 생성: + +```json +{ + "icons": { + "combat": "f98db6823e894a4f90308d61f75894ac", + "elite": "793ed8a757534b89a82f460747d2df24", + "boss": "423056cdbbc04f4da131b9721c404d96", + "shop": "da37e1fac55d455b9ade08569f09f798", + "rest": "b86c1b0568bd45f3ae4a4b97e1b4a594", + "treasure": "f8a6d58e20f54e2ca899485055df1ce4" + }, + "background": "d84241f17de344a097f5b96ac914f1d2" +} +``` + +- [ ] **Step 2:** `gen-slaydeck.mjs` CHEST 상수(`85`) 아래에 로드+검증 추가: + +```js +// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성) +const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8')); +for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) { + if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`); +} +if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류'); +``` + +- [ ] **Step 3:** `luaFramesTable`(`77`) 직후에 직렬화 헬퍼 추가: + +```js +function luaNodeIconsTable() { + const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n'); + return `self.NodeIcons = {\n${rows}\n}`; +} +``` + +- [ ] **Step 4:** prop 선언 추가 — `prop('any', 'CardFrames'),`(`2854`) 아래에 `prop('any', 'NodeIcons'),`. + +- [ ] **Step 5:** OnBeginPlay 주입 — `2906`의 `${luaFramesTable()}` 줄 **아래**에 `${luaNodeIconsTable()}` 추가. StartRun 주입(`3361`)의 `${luaFramesTable()}` 아래에도 동일 추가. + +- [ ] **Step 6:** 로드 검증(아직 산출물 미변경이라 생성만 확인): + +```bash +node -e "const n=require('./data/nodeicons.json'); console.log('icons',Object.keys(n.icons).join(','),'| bg',n.background.length)" +``` +기대: `icons combat,elite,boss,shop,rest,treasure | bg 32` + +- [ ] **Step 7:** 커밋: + +```bash +git add data/nodeicons.json tools/deck/gen-slaydeck.mjs +git commit -m "feat(node-map): nodeicons.json 외부화 + 생성기 로드·검증·NodeIcons 직렬화" +``` + +--- + +### Task 2: MapHud emit — 배경 이미지 + 오버레이 + 아이콘 노드 + +**Files:** Modify `tools/deck/gen-slaydeck.mjs` + +- [ ] **Step 1:** MapHud 루트 sprite(`1673`)를 **배경 이미지**로 변경: + +```js + sprite({ dataId: NODEICONS.background, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), +``` + +- [ ] **Step 2:** 루트 push(`1677` `map.push(mapHud);`) 직후, Title push 앞에 **반투명 오버레이 자식** 추가: + +```js + map.push(entity({ + id: guid('map', 990), + path: '/ui/DefaultGroup/MapHud/Overlay', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.04, g: 0.05, b: 0.09, a: 0.5 }, type: 1, raycast: true }), + ], + })); +``` +(guid 'map',990 은 노드 그리드·도트가 쓰는 mapN(2~약189)보다 충분히 높아 충돌 없음. 빌드 끝 id 유일성 검증이 잡아줌.) + +- [ ] **Step 3:** Title displayOrder를 오버레이(0) 위로 — Title 엔티티(`1684` `displayOrder: 0,`)를 `displayOrder: 2,`로 변경. + +- [ ] **Step 4:** `pushMapNode`(`1696~1726`) — 노드 본체를 **아이콘**으로 + Label 자식 제거: + - 본체 sprite(`1707`)를 `sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }),`로 변경(단색 박스 → 이미지, 런타임에 ImageRUID 주입). + - Label 자식 push 블록(`1713~1725`, `map.push(entity({ ... /Label ... }))` 전체)을 **삭제**. + +- [ ] **Step 5:** 노드 크기 키움 — 그리드 호출(`1729`)의 `{ x: 56, y: 56 }`을 `{ x: 64, y: 64 }`로, 보스 호출(`1732`)의 `{ x: 72, y: 72 }`을 `{ x: 88, y: 88 }`로 변경. + +- [ ] **Step 6:** 커밋(아직 RenderMapNode 미수정 — 다음 Task와 함께 재생성/검증): + +```bash +git add tools/deck/gen-slaydeck.mjs +git commit -m "feat(node-map): MapHud 배경 이미지+오버레이, 노드 아이콘화(라벨 제거·확대)" +``` + +--- + +### Task 3: RenderMapNode Lua — ImageRUID + 상태 틴트 + +**Files:** Modify `tools/deck/gen-slaydeck.mjs` + +- [ ] **Step 1:** `RenderMapNode` 메서드 본문(`5615~5677`)을 아래로 **교체**(타입별 박스색/라벨 → 아이콘 ImageRUID + 상태 틴트). Lua 들여쓰기는 기존과 동일하게 실제 탭: + +```lua +local base = "/ui/DefaultGroup/MapHud/Node_" .. id +local e = _EntityService:GetEntityByPath(base) +if e == nil then + return +end +local node = self.MapNodes[id] +if node == nil then + e.Enable = false + return +end +e.Enable = true +local ruid = self.NodeIcons[node.type] +if ruid == nil then + ruid = self.NodeIcons["combat"] +end +if e.SpriteGUIRendererComponent ~= nil and ruid ~= nil then + e.SpriteGUIRendererComponent.ImageRUID = ruid +end +local reachable = self:IsReachable(id) +local visited = false +if self.VisitedNodes ~= nil then + for i = 1, #self.VisitedNodes do + if self.VisitedNodes[i] == id then visited = true end + end +end +if e.SpriteGUIRendererComponent ~= nil then + if id == self.CurrentNodeId then + e.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) + elseif visited == true then + e.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.9) + elseif reachable == true then + e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) + else + e.SpriteGUIRendererComponent.Color = Color(0.4, 0.4, 0.45, 0.45) + end +end +if e.ButtonComponent ~= nil then + e.ButtonComponent.Enable = reachable +end +``` +(메서드 시그니처 `[{Type:'string',...,Name:'id'}]`는 유지. `self:SetText(base.."/Label", ...)` 호출은 라벨 제거로 사라짐 — RenderMapDots/RenderMap는 불변.) + +- [ ] **Step 2:** 재생성: + +```bash +node tools/deck/gen-slaydeck.mjs +``` +기대: "Slay deck UI and combat codeblocks generated." + +- [ ] **Step 3:** 카운트 검증(내용 출력 금지, node fs): + +```bash +node -e "const fs=require('fs');const cb=fs.readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8');const ui=fs.readFileSync('ui/DefaultGroup.ui','utf8');const c=(s,p)=>(s.match(new RegExp(p,'g'))||[]).length;console.log('NodeIcons inject:',c(cb,'self.NodeIcons ='),'(>=2: OnBeginPlay+StartRun)','| ImageRUID in RenderMapNode:',c(cb,'NodeIcons\\\\[node.type\\\\]'),'| UI MapHud/Overlay:',c(ui,'MapHud/Overlay'),'(1)','| UI Label nodes(0 기대):',c(ui,'Node_r1c1/Label'),'| bg RUID:',c(ui,'d84241f17de344a097f5b96ac914f1d2'));" +``` +기대: NodeIcons inject ≥2, ImageRUID ≥1, Overlay 1, Label 0, bg RUID ≥1. + +- [ ] **Step 4:** 커밋: + +```bash +git add tools/deck/gen-slaydeck.mjs ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock +git commit -m "feat(node-map): RenderMapNode 아이콘 ImageRUID+상태 틴트, 재생성" +``` + +--- + +### Task 4: 미러/회귀 테스트 + +- [ ] **Step 1:** 전투/맵그래프 미러 미변경 확인 — 테스트 실행: + +```bash +node --test tools/balance/sim-balance.test.mjs tools/map/rogue-map.test.mjs +``` +기대: 전부 PASS(이 변경은 UI만, 전투/맵그래프 무관). + +- [ ] **Step 2:** `git status --short`로 의도치 않은 산출물 변경 없는지 확인. + +--- + +### Task 5: 메이커 플레이테스트 + +- [ ] **Step 1:** `maker_refresh_workspace` → `maker_logs build`로 빌드 에러 0 확인(기존 BuySoulUnlock Info 경고는 무관). + +- [ ] **Step 2:** `maker_play` → 런 시작(`SelectClass`+`StartNewGame`) → 맵 화면 `maker_screenshot`. 검증: + - 배경 이미지(리스항구) + 어두운 오버레이 위에 노드들. + - 노드가 **타입별 아이콘**(주황버섯/골렘/발록/돈주머니/모닥불/상자)으로 표시, 라벨 텍스트 없음. + - 상태 틴트: 현재=금색, 도달가능=원색(밝게), 잠김=어둡고 흐릿. + - 도달 가능 노드 클릭 시 진행(`PickNode`/마우스). 랜덤 배치 정상. + - 아이콘 잘림/왜곡 점검(특히 보스 발록·골렘). 잘리면 해당 노드 size 또는 아이콘 RUID 조정. + +- [ ] **Step 2b:** 실패 시 디버깅 — 흰박스→RUID/리로드 확인, 아이콘 안 뜸→ImageRUID 주입·NodeIcons 시드 확인, 가독성→오버레이 알파/틴트 튜닝. 생성기 수정→재생성→refresh→재플레이. + +- [ ] **Step 3:** `maker_stop`. 스크린샷 사용자 공유. + +--- + +### Task 6: PR + +- [ ] **Step 1:** `git push -u origin feature/node-map-ui`(인증 실패 시 `GCM_INTERACTIVE=never GIT_TERMINAL_PROMPT=0 git push`로 재시도). +- [ ] **Step 2:** UTF-8 spec JSON 작성 후 `node tools/git/gitea-pr.mjs create `. 제목 "feat: 노드 맵 UI 강화 — 아이콘 노드 + 배경 이미지(nodeicons.json 외부화)". +- [ ] **Step 3:** 사용자에게 PR 번호 보고. (변경 용이성: `data/nodeicons.json` RUID만 바꾸고 `node tools/deck/gen-slaydeck.mjs` 재실행하면 교체됨을 명시.)