Files
maplecontest/docs/superpowers/plans/2026-06-12-card-frames.md

4.8 KiB
Raw Permalink Blame History

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: 리소스 커밋

  • .sprite 9종 커밋: 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.json 32종에 "rarity" 추가 (설계서 표 그대로 — node 스크립트로 일괄 주입 권장)
  • 커밋: feat(card-frames): 카드 등급 배정·프레임 RUID 매핑 데이터

Task 3: 생성기 — 프레임 렌더링

  • CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json')) 로드, 카드별 검증(throw): rarity ∈ {normal,unique,legend}, class ∈ classToFrame
  • luaCardsTable: fields.push(\rarity = ${luaStr(c.rarity)}`)`
  • OnBeginPlay 주입(luaCardsTable 옆): luaFramesTable()self.CardFrames = {...} + self.ClassToFrame = {...} / prop('any','CardFrames')·prop('any','ClassToFrame') 선언
  • ApplyCardFace Lua: 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 확인
  • OfferReward Lua 교체:
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.mjsgrep -c "CardFrames" RootDesk/MyDesk/SlayDeckController.codeblock ≥1, grep -c "4bb57ef88ef449fdaf958f6cf37fe44b" ui/DefaultGroup.ui ≥1
  • node --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만 사용 ✓