Files
maplecontest/docs/superpowers/plans/2026-06-06-card-image-slot.md
2026-06-06 11:54:44 +09:00

236 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 카드 슬롯 이미지 적용 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)"
```
출력된 바이트 수를 `<BYTES>`로 사용한다.
- [ ] **Step 2: 업로드 1단계 — presigned URL 발급**
MCP `asset_create_account_resource_storage_item` 호출:
- `category`: `sprite`
- `subcategory`: `etc`
- `name`: `slaymaple_card_reboot_protocol`
- `description`: `SlayMaple 손패 카드 이미지 (리부트 프로토콜)`
- `contentLength`: `<BYTES>`
- `fileUrl`: 생략
응답에서 `presignedUrl``<PRESIGNED_URL>`로 확보.
- [ ] **Step 3: 파일 PUT 업로드**
```bash
curl.exe -X PUT --data-binary "@C:/Users/jaeoh/Desktop/workspace/source/images/maple/invincible belief.png" "<PRESIGNED_URL>"
```
Expected: HTTP 200 (출력 없음 또는 빈 본문). 오류 시 응답 본문 확인.
- [ ] **Step 4: 업로드 2단계 — 리소스 생성 완료**
MCP `asset_create_account_resource_storage_item` 다시 호출, 이번엔 동일 파라미터 + `fileUrl`: `<PRESIGNED_URL>`.
응답에서 발급된 리소스 **RUID(GUID/DataId)**`<RUID>`로 확보.
- [ ] **Step 5: RUID 검증**
MCP `asset_list_account_resources` (`category`: `sprite`, `subcategory`: `etc`, `searchWord`: `reboot`) 호출 → 방금 만든 리소스가 목록에 있고 RUID가 일치하는지 확인. `<RUID>`를 기록해 Task 2에서 사용.
---
### Task 2: 생성기에 image 분기 추가
**Files:**
- Modify: `tools/gen-cardhand.mjs`
- [ ] **Step 1: 5번 카드 데이터에 image 필드 추가**
`cards` 배열의 마지막 원소(강타)를 다음으로 교체 (`<RUID>`는 Task 1에서 확보한 실제 값):
```js
{ name: '강타', cost: '2', desc: '피해 10', tint: ATTACK, image: '<RUID>' },
```
- [ ] **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:``<RUID>` 와 일치
- `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=<RUID>` 확인. (이미지 미로드 시 `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 확인 + 스크린샷 시각 확인