80 lines
4.8 KiB
Markdown
80 lines
4.8 KiB
Markdown
# 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 틴트 분기 → 프레임 적용
|
||
|
||
```lua
|
||
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 교체:
|
||
|
||
```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.mjs` → `grep -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만 사용 ✓
|