11 KiB
노드 맵 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생성:
{
"icons": {
"combat": "f98db6823e894a4f90308d61f75894ac",
"elite": "793ed8a757534b89a82f460747d2df24",
"boss": "423056cdbbc04f4da131b9721c404d96",
"shop": "da37e1fac55d455b9ade08569f09f798",
"rest": "b86c1b0568bd45f3ae4a4b97e1b4a594",
"treasure": "f8a6d58e20f54e2ca899485055df1ce4"
},
"background": "d84241f17de344a097f5b96ac914f1d2"
}
- Step 2:
gen-slaydeck.mjsCHEST 상수(85) 아래에 로드+검증 추가:
// 노드 맵 아이콘/배경 (공식 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) 직후에 직렬화 헬퍼 추가:
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: 로드 검증(아직 산출물 미변경이라 생성만 확인):
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: 커밋:
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)를 배경 이미지로 변경:
sprite({ dataId: NODEICONS.background, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
- Step 2: 루트 push(
1677map.push(mapHud);) 직후, Title push 앞에 반투명 오버레이 자식 추가:
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 엔티티(
1684displayOrder: 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 ... }))전체)을 삭제.
- 본체 sprite(
-
Step 5: 노드 크기 키움 — 그리드 호출(
1729)의{ x: 56, y: 56 }을{ x: 64, y: 64 }로, 보스 호출(1732)의{ x: 72, y: 72 }을{ x: 88, y: 88 }로 변경. -
Step 6: 커밋(아직 RenderMapNode 미수정 — 다음 Task와 함께 재생성/검증):
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 들여쓰기는 기존과 동일하게 실제 탭:
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: 재생성:
node tools/deck/gen-slaydeck.mjs
기대: "Slay deck UI and combat codeblocks generated."
- Step 3: 카운트 검증(내용 출력 금지, node fs):
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: 커밋:
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: 전투/맵그래프 미러 미변경 확인 — 테스트 실행:
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.jsonRUID만 바꾸고node tools/deck/gen-slaydeck.mjs재실행하면 교체됨을 명시.)