228 lines
11 KiB
Markdown
228 lines
11 KiB
Markdown
# 노드 맵 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 <spec.json>`. 제목 "feat: 노드 맵 UI 강화 — 아이콘 노드 + 배경 이미지(nodeicons.json 외부화)".
|
|
- [ ] **Step 3:** 사용자에게 PR 번호 보고. (변경 용이성: `data/nodeicons.json` RUID만 바꾸고 `node tools/deck/gen-slaydeck.mjs` 재실행하면 교체됨을 명시.)
|