9.9 KiB
카드 슬롯 이미지 적용 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: 파일 크기 확인
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:spritesubcategory:etcname:slaymaple_card_reboot_protocoldescription:SlayMaple 손패 카드 이미지 (리부트 프로토콜)contentLength:<BYTES>fileUrl: 생략
응답에서 presignedUrl을 <PRESIGNED_URL>로 확보.
- Step 3: 파일 PUT 업로드
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에서 확보한 실제 값):
{ name: '강타', cost: '2', desc: '피해 10', tint: ATTACK, image: '<RUID>' },
- Step 2: 카드 빌드 루프를 image 분기로 교체
cards.forEach((c, i) => { ... }); 블록 전체(현재 카드 배경 + cost/name/desc 생성)를 다음으로 교체:
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: 스크립트 커밋
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이므로 베이스가 필요)
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장 텍스트 유지 검증
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 유효성 + 기존(우리 외) 엔티티 불변
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 모드). Expectedstatus: ok. - Step 2: play —
maker_play. - Step 3: 로드 확인 —
maker_execute_script(context client):→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)) endmaker_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: 디스크 무결성 후 커밋
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 확인 + 스크린샷 시각 확인