From 6a049def850839d547e59ac0e2da075cdc48ad28 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 6 Jun 2026 11:54:44 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=8A=AC=EB=A1=AF=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=81=EC=9A=A9=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EA=B3=84=ED=9A=8D=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= 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-06-card-image-slot.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-06-card-image-slot.md diff --git a/docs/superpowers/plans/2026-06-06-card-image-slot.md b/docs/superpowers/plans/2026-06-06-card-image-slot.md new file mode 100644 index 0000000..9c2445f --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-card-image-slot.md @@ -0,0 +1,235 @@ +# 카드 슬롯 이미지 적용 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 하단 손패 5번 슬롯(강타)의 외형을 완성형 카드 이미지 `invincible belief.png`("리부트 프로토콜")로 교체한다. + +**Architecture:** PNG를 MSW 계정 sprite 리소스로 업로드해 RUID를 발급받고, 그 RUID를 생성기 `gen-cardhand.mjs`의 5번 카드 데이터에 `image` 필드로 넣는다. 생성기는 `image`가 있는 카드를 단색 배경 대신 해당 RUID 스프라이트(흰색 틴트, 180×270)로 만들고 텍스트 자식을 생성하지 않는다. 재생성 후 Maker reload→play 스크린샷으로 검증한다. + +**Tech Stack:** MSW 에셋 MCP(`asset_create_account_resource_storage_item`, 2단계 업로드), curl PUT, Node.js 생성기, msw-maker-mcp(reload/play/screenshot). + +--- + +## File Structure + +- Modify: `tools/gen-cardhand.mjs` — 카드 빌드 루프에 `image` 분기 추가, 5번 카드에 RUID 부여. +- Modify: `ui/DefaultGroup.ui` — 생성기가 5번 카드를 이미지 스프라이트로 재생성. +- 외부: MSW 계정 리소스 스토리지에 PNG 업로드(저장소엔 RUID만 들어감). + +원본 이미지: `C:\Users\jaeoh\Desktop\workspace\source\images\maple\invincible belief.png` (세로 약 2:3, 완성형 카드). + +--- + +### Task 1: 이미지 업로드 및 RUID 확보 (컨트롤러/MCP 실행) + +**Files:** 없음 (외부 리소스 생성) + +- [ ] **Step 1: 파일 크기 확인** + +```bash +node -e "console.log(require('fs').statSync('C:/Users/jaeoh/Desktop/workspace/source/images/maple/invincible belief.png').size)" +``` +출력된 바이트 수를 ``로 사용한다. + +- [ ] **Step 2: 업로드 1단계 — presigned URL 발급** + +MCP `asset_create_account_resource_storage_item` 호출: +- `category`: `sprite` +- `subcategory`: `etc` +- `name`: `slaymaple_card_reboot_protocol` +- `description`: `SlayMaple 손패 카드 이미지 (리부트 프로토콜)` +- `contentLength`: `` +- `fileUrl`: 생략 + +응답에서 `presignedUrl`을 ``로 확보. + +- [ ] **Step 3: 파일 PUT 업로드** + +```bash +curl.exe -X PUT --data-binary "@C:/Users/jaeoh/Desktop/workspace/source/images/maple/invincible belief.png" "" +``` +Expected: HTTP 200 (출력 없음 또는 빈 본문). 오류 시 응답 본문 확인. + +- [ ] **Step 4: 업로드 2단계 — 리소스 생성 완료** + +MCP `asset_create_account_resource_storage_item` 다시 호출, 이번엔 동일 파라미터 + `fileUrl`: ``. +응답에서 발급된 리소스 **RUID(GUID/DataId)** 를 ``로 확보. + +- [ ] **Step 5: RUID 검증** + +MCP `asset_list_account_resources` (`category`: `sprite`, `subcategory`: `etc`, `searchWord`: `reboot`) 호출 → 방금 만든 리소스가 목록에 있고 RUID가 일치하는지 확인. ``를 기록해 Task 2에서 사용. + +--- + +### Task 2: 생성기에 image 분기 추가 + +**Files:** +- Modify: `tools/gen-cardhand.mjs` + +- [ ] **Step 1: 5번 카드 데이터에 image 필드 추가** + +`cards` 배열의 마지막 원소(강타)를 다음으로 교체 (``는 Task 1에서 확보한 실제 값): + +```js + { name: '강타', cost: '2', desc: '피해 10', tint: ATTACK, image: '' }, +``` + +- [ ] **Step 2: 카드 빌드 루프를 image 분기로 교체** + +`cards.forEach((c, i) => { ... });` 블록 전체(현재 카드 배경 + cost/name/desc 생성)를 다음으로 교체: + +```js +cards.forEach((c, i) => { + const cardPath = `/ui/DefaultGroup/CardHand/Card${i + 1}`; + const cardH = c.image ? 270 : CARD_H; + const cardSprite = c.image + ? sprite({ dataId: c.image, color: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, type: 0, raycast: true }) + : sprite({ color: c.tint, type: 1, raycast: true }); + // card background (or full image) + ents.push(entity({ + id: guid(g++), + path: cardPath, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: i, + components: [ + transform({ parentW: 1020, parentH: 280, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: cardH }, pos: { x: (i - (cards.length - 1) / 2) * CARD_SPACING, y: 0 }, align: ALIGN_CENTER }), + cardSprite, + ], + })); + // 이미지 카드는 텍스트 오버레이를 만들지 않는다 (이미지에 이미 포함) + if (c.image) return; + // cost (top-left) + ents.push(entity({ + id: guid(g++), + path: `${cardPath}/Cost`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 50, y: 50 }, pos: { x: -60, y: 95 } }), + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + text({ value: c.cost, fontSize: 34, bold: true }), + ], + })); + // name (upper-center) + ents.push(entity({ + id: guid(g++), + path: `${cardPath}/Name`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 50 }, pos: { x: 0, y: 50 } }), + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + text({ value: c.name, fontSize: 28, bold: true }), + ], + })); + // desc (lower-center) + ents.push(entity({ + id: guid(g++), + path: `${cardPath}/Desc`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 80 }, pos: { x: 0, y: -80 } }), + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + text({ value: c.desc, fontSize: 22, bold: false }), + ], + })); +}); +``` + +- [ ] **Step 3: 스크립트 커밋** + +```bash +cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" +git add tools/gen-cardhand.mjs +git commit -m "카드 손패 생성기: image 필드 지원 (5번 카드 이미지 적용)" +``` + +--- + +### Task 3: 재생성 및 데이터 검증 + +**Files:** +- Modify: `ui/DefaultGroup.ui` + +- [ ] **Step 1: 카드 없는 베이스로 되돌린 뒤 재생성** + +직전 카드 커밋(`c9c761d`) 이전 베이스에서 ui를 받아 재생성한다. (생성기는 CardHand가 이미 있으면 no-op이므로 베이스가 필요) + +```bash +cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" +git checkout 2c39066 -- ui/DefaultGroup.ui +node tools/gen-cardhand.mjs +``` +Expected: `Inserted 18 CardHand entities.` (컨테이너 1 + 카드 5 + 텍스트 12 = 18) + +- [ ] **Step 2: 5번 카드 = 이미지, 텍스트 없음 / 나머지 4장 텍스트 유지 검증** + +```bash +cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" +node -e "const j=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const E=j.ContentProto.Entities;const card5=E.find(e=>e.path==='/ui/DefaultGroup/CardHand/Card5');const sp=card5.jsonString['@components'][1];const tr=card5.jsonString['@components'][0];console.log('card5 image:', sp.ImageRUID.DataId);console.log('card5 height:', tr.RectSize.y);console.log('card5 has text children:', E.some(e=>e.path.startsWith('/ui/DefaultGroup/CardHand/Card5/')));console.log('card1 has text children:', E.some(e=>e.path.startsWith('/ui/DefaultGroup/CardHand/Card1/')));console.log('total CardHand entities:', E.filter(e=>e.path.includes('CardHand')).length)" +``` +Expected: +- `card5 image:` 가 `` 와 일치 +- `card5 height: 270` +- `card5 has text children: false` +- `card1 has text children: true` +- `total CardHand entities: 18` + +- [ ] **Step 3: JSON 유효성 + 기존(우리 외) 엔티티 불변** + +```bash +cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" +node -e "JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));console.log('JSON ok')" +``` +Expected: `JSON ok`. (Button_Attack/Jump/UIJoystick/UIChat 4개 기본 엔티티는 splice가 끝에만 추가하므로 불변) + +--- + +### Task 4: Maker 시각 검증 (컨트롤러 실행) + +**Files:** 없음 + +- [ ] **Step 1: reload** — msw-maker-mcp `maker_refresh_workspace` (edit 모드). Expected `status: ok`. +- [ ] **Step 2: play** — `maker_play`. +- [ ] **Step 3: 로드 확인** — `maker_execute_script` (context client): + ```lua + local c5 = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card5") + log("CARD5="..tostring(c5 ~= nil)) + if c5 ~= nil then local r = c5.SpriteGUIRendererComponent; log("CARD5_IMG="..tostring(r.ImageRUID.DataId)) end + ``` + → `maker_logs(kind=normal)` 에서 `CARD5=true`, `CARD5_IMG=` 확인. (이미지 미로드 시 `maker_logs(kind=build)` 도 확인) +- [ ] **Step 4: screenshot** — `maker_screenshot` 후 Read로 열어 5번 자리에 "리부트 프로토콜" 카드 이미지가 왜곡 없이 표시되는지 확인. 나머지 4장은 단색 목업 유지. +- [ ] **Step 5: stop** — `maker_stop`. + +문제 시(이미지 안 보임/깨짐): subcategory를 `item`으로 바꿔 재업로드하거나, 스프라이트 Type/PreserveSprite를 조정. ui 되돌리기: `git checkout ui/DefaultGroup.ui` 후 Task 3부터 재실행. + +--- + +### Task 5: 최종 커밋 + +**Files:** +- `ui/DefaultGroup.ui` + +- [ ] **Step 1: 디스크 무결성 후 커밋** + +```bash +cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" +git add ui/DefaultGroup.ui +git commit -m "5번 카드 슬롯에 리부트 프로토콜 이미지 카드 적용" +``` + +--- + +## 검증 요약 +- RUID 발급/검증 (asset_list) +- 생성기: `Inserted 18`, 5번=이미지·텍스트없음·270, 나머지 텍스트 유지, JSON 유효 +- Maker: Lua로 Card5 이미지 RUID 확인 + 스크린샷 시각 확인