4.8 KiB
4.8 KiB
P13 — 커스텀 카드 프레임 구현 계획
For agentic workers: REQUIRED SUB-SKILL: superpowers:executing-plans. 설계:
2026-06-12-card-frames-design.md
Goal: 사용자 제작 프레임 이미지(직업×등급)를 카드 UI 전체에 적용하고 등급을 보상 확률에 반영.
Architecture: 단일 소스(data/*.json + gen-slaydeck.mjs) → 산출물 재생성. 카드 배경 스프라이트를 프레임 ImageRUID로 교체(A안), ApplyCardFace 중앙 함수에서 class×rarity 조회.
Tech Stack: Node.js 생성기, MSW Lua, node --test.
Task 1: 리소스 커밋
.sprite9종 커밋:git add RootDesk/MyDesk/*.sprite && git commit -m "feat(card-frames): 카드 프레임 스프라이트 9종 로컬 임포트 (warior·mage·bandit × normal·unique·legend)"
Task 2: 데이터 — rarity + cardframes.json
data/cardframes.json신설 (설계서 JSON 그대로)data/cards.json32종에"rarity"추가 (설계서 표 그대로 — node 스크립트로 일괄 주입 권장)- 커밋:
feat(card-frames): 카드 등급 배정·프레임 RUID 매핑 데이터
Task 3: 생성기 — 프레임 렌더링
CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json'))로드, 카드별 검증(throw): rarity ∈ {normal,unique,legend}, class ∈ classToFrameluaCardsTable:fields.push(\rarity = ${luaStr(c.rarity)}`)`- OnBeginPlay 주입(luaCardsTable 옆):
luaFramesTable()—self.CardFrames = {...}+self.ClassToFrame = {...}/prop('any','CardFrames')·prop('any','ClassToFrame')선언 ApplyCardFaceLua: kind 틴트 분기 → 프레임 적용
local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"]
local ruid = frames ~= nil and frames[c.rarity or "normal"] or nil
if ruid ~= nil then
e.SpriteGUIRendererComponent.ImageRUID = ruid
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
end
cardFaceLayout(W)헬퍼 신설(s=W/180): Cost pos(-68s,103s)/size 44s/font 26s · Name pos(4s,97s)/size(150s,26s)/font 18s · Art pos(0,16s)/size 110s · Desc pos(0,-85s)/size(152s,64s)/font 16s- 카드 생성 5곳(upsertUi 손패 ~523 · 조회 ~787 · 전체덱 ~928 · 보상 ~1443 · 상점 ~1660)에 헬퍼 적용, NamePlate/CostPlate 생성 제거, 카드 스프라이트 type 0·흰색·프리뷰 프레임 RUID
- CardHand 잔존 단색판 제거: upsertUi 초입 필터에
/ui/DefaultGroup/CardHand/Card\d+/(NamePlate|CostPlate)경로 제거 추가 - 커밋:
feat(card-frames): 생성기 — 프레임 렌더링·레이아웃 통합
Task 4: 보상 가중 추첨 (TDD)
tools/balance/sim-balance.test.mjs에 실패 테스트:rarityForRoll(70)==='normal',(71)==='unique',(95)==='unique',(96)==='legend'→ 실행해 FAIL 확인tools/balance/sim-balance.mjs:export function rarityForRoll(roll){ if (roll > 95) return 'legend'; if (roll > 70) return 'unique'; return 'normal'; }→ PASS 확인OfferRewardLua 교체:
local pool = self:CardPool()
local byRarity = {}
for _, id in ipairs(pool) do
local r = self.Cards[id].rarity or "normal"
if byRarity[r] == nil then byRarity[r] = {} end
table.insert(byRarity[r], id)
end
self.RewardChoices = {}
for i = 1, 3 do
local roll = math.random(1, 100)
local want = "normal"
if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end
local bucket = byRarity[want]
if bucket == nil or #bucket == 0 then bucket = pool end
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
self:ApplyRewardVisual(i, self.RewardChoices[i])
end
- 커밋:
feat(card-frames): 보상 등급 가중 추첨 70/25/5 (+JS 미러 테스트)
Task 5: 재생성·검증·산출물 커밋
node tools/deck/gen-slaydeck.mjs→grep -c "CardFrames" RootDesk/MyDesk/SlayDeckController.codeblock≥1,grep -c "4bb57ef88ef449fdaf958f6cf37fe44b" ui/DefaultGroup.ui≥1node --test tools/balance/sim-balance.test.mjs tools/map/rogue-map.test.mjs전건 통과- 커밋:
feat(card-frames): 산출물 재생성
Task 6: 메이커 검증·튜닝
- maker_refresh_workspace → 빌드 콘솔 0에러 → 플레이: 손패 프레임·등급 구분,
_ResourceService로드 확인, 보상·덱 조회 스크린샷 - 텍스트/아트 위치 어긋나면
cardFaceLayout수치 조정 → 재생성 → 재확인 (수정 시 커밋)
Task 7: PR·머지·메모리
- push →
node tools/git/gitea-pr.mjs create <spec.json>→ merge → main pull → 메모리 갱신 (slaymaple-build-status에 P13 추가)
Self-Review
- 설계 전 항목에 대응 Task 존재 ✓ / 코드 블록 placeholder 없음 ✓ / CardFrames·ClassToFrame·rarityForRoll 명칭 일관 ✓ / maker_save 덮어쓰기 주의(설계서 '주의' 절) Task 6에서 refresh만 사용 ✓