docs(plan): 직업 선택 캐릭터 이미지 + 뒤로가기 구현 계획
This commit is contained in:
205
docs/superpowers/plans/2026-06-16-charselect-images-back.md
Normal file
205
docs/superpowers/plans/2026-06-16-charselect-images-back.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# 직업 선택 캐릭터 이미지 + 뒤로가기 — 구현 계획
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans 로 태스크 단위 구현. 단계는 `- [ ]` 체크박스.
|
||||||
|
|
||||||
|
**Goal:** CharacterSelectHud의 단색 박스를 캐릭터 이미지 카드로 바꾸고(선택 시 금색 테두리), 뒤로가기 버튼으로 로비 복귀를 추가한다.
|
||||||
|
|
||||||
|
**Architecture:** 단일 생성기 `tools/deck/gen-slaydeck.mjs` 수정 + `data/characters.json` 신설(초상화 RUID 단일 소스). 이미지는 생성 시 `sprite({dataId})`로 주입, 선택 표시는 기존 `RenderCharacterSelect`의 Button Color를 금색으로. 뒤로가기는 ShopHud 나가기 패턴 재사용 → `ShowLobby()`. 산출물(ui/codeblock) 재생성.
|
||||||
|
|
||||||
|
**Tech Stack:** Node ESM 생성기, MSW Lua codeblock, MSW UI JSON. 검증=카운트+메이커 플레이테스트(이 저장소는 단위테스트 대신 카운트/플레이테스트).
|
||||||
|
|
||||||
|
**확정 RUID (메이커 임포트 완료, `.sprite`에서 추출):**
|
||||||
|
- warrior `28c88fdc5ab44f34a8b3fc1e19d4ce78`
|
||||||
|
- magician `3b9ea1f066a744bb859df47fef817277`
|
||||||
|
- bandit `efa920e58d31426486ef974106e7dc8b`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `data/characters.json` + 생성기 로드·검증
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `data/characters.json`
|
||||||
|
- Modify: `tools/deck/gen-slaydeck.mjs:91-96` 인접(NODEICONS 로드 블록 뒤)
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `data/characters.json` 작성
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"portraits": {
|
||||||
|
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
|
||||||
|
"magician": "3b9ea1f066a744bb859df47fef817277",
|
||||||
|
"bandit": "efa920e58d31426486ef974106e7dc8b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** gen-slaydeck.mjs NODEICONS 검증 블록(`:96`) 바로 뒤에 로드+fail-fast 검증 추가
|
||||||
|
```js
|
||||||
|
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
|
||||||
|
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
|
||||||
|
for (const c of ['warrior', 'magician', 'bandit']) {
|
||||||
|
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** 생성기 실행해 에러 없는지 확인(아직 UI 미사용이라 출력 동일)
|
||||||
|
```
|
||||||
|
node tools/deck/gen-slaydeck.mjs
|
||||||
|
```
|
||||||
|
Expected: 성공 메시지 1줄, throw 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: CharacterSelectHud — 카드 이미지화 (classCards 루프)
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/gen-slaydeck.mjs:2516-2540` (Portrait/Desc 블록), `:2503-2515` (Name)
|
||||||
|
|
||||||
|
카드 본체 `{key}Button`(2490-2502)·DeckButton(2567-2580)·StartButton·click 바인딩 경로는 **불변**. `cls.tint`/`cls.desc`는 더는 안 쓰이나 배열 정의는 그대로 둬도 무방.
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `Name`(2503-2515) 위치를 하단으로 — `transform`의 `pos: { x: 0, y: 108 }` → `pos: { x: 0, y: -137 }`. (displayOrder 0 유지) — 텍스트는 그대로(금색).
|
||||||
|
|
||||||
|
- [ ] **Step 2:** `Portrait` 엔티티(2516-2527)를 **`Art` 이미지로 교체**. 경로·guid·sprite 변경:
|
||||||
|
```js
|
||||||
|
select.push(entity({
|
||||||
|
id: guid('menu', 200 + i),
|
||||||
|
path: `${base}/Art`,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: 0,
|
||||||
|
components: [
|
||||||
|
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 318 }, pos: { x: 0, y: 0 } }),
|
||||||
|
sprite({ dataId: CHARS.portraits[cls.classId], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
(258×318, 6px 인셋 → 부모 Button 색이 테두리로 보임. type:0=이미지 풀, raycast off=클릭은 부모 Button으로.)
|
||||||
|
|
||||||
|
- [ ] **Step 3:** `Desc` 엔티티(2528-2540) **삭제**(emit 안 함).
|
||||||
|
|
||||||
|
- [ ] **Step 4:** `Name` 뒤에 반투명 하단 배너 `NameBanner` 추가(displayOrder 1, Art 위·Name 아래). Name의 displayOrder를 2로 올림.
|
||||||
|
```js
|
||||||
|
select.push(entity({
|
||||||
|
id: guid('menu', 210 + i),
|
||||||
|
path: `${base}/NameBanner`,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: 1,
|
||||||
|
components: [
|
||||||
|
transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 60 }, pos: { x: 0, y: -137 } }),
|
||||||
|
sprite({ color: { r: 0, g: 0, b: 0, a: 0.55 }, type: 1, raycast: false }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
그리고 Name 엔티티의 `displayOrder: 0` → `displayOrder: 2`로.
|
||||||
|
|
||||||
|
- [ ] **Step 5:** 생성 + 카운트 검증
|
||||||
|
```
|
||||||
|
node tools/deck/gen-slaydeck.mjs
|
||||||
|
node tools/verify/count.mjs ui "CharacterSelectHud/WarriorButton/Art" "CharacterSelectHud/MageButton/Art" "CharacterSelectHud/ThiefButton/Art"
|
||||||
|
grep -c "28c88fdc5ab44f34a8b3fc1e19d4ce78" ui/DefaultGroup.ui # warrior RUID 1
|
||||||
|
```
|
||||||
|
Expected: Art 3개 존재, RUID 등장. (count.mjs 없으면 `grep -c '/Art"' ui/DefaultGroup.ui`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: RenderCharacterSelect — 선택 = 금색 테두리
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/gen-slaydeck.mjs:3362-3394`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 선택 시 색을 금색으로. 세 군데 `Color(0.28, 0.36, 0.46, 1)` → `Color(1, 0.82, 0.3, 1)` (미선택 `Color(0.16, 0.2, 0.26, 1)`는 유지). Status 텍스트 로직 불변.
|
||||||
|
- `gen-slaydeck.mjs`에서 `Color(0.28, 0.36, 0.46, 1)` 를 `Color(1, 0.82, 0.3, 1)` 로 (RenderCharacterSelect 내 3회) 치환.
|
||||||
|
|
||||||
|
- [ ] **Step 2:** 생성 + 확인
|
||||||
|
```
|
||||||
|
node tools/deck/gen-slaydeck.mjs
|
||||||
|
grep -c "Color(1, 0.82, 0.3, 1)" RootDesk/MyDesk/SlayDeckController.codeblock # 증가 확인(기존 사용처 + 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 뒤로가기 버튼 + 바인딩
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/gen-slaydeck.mjs` — CharacterSelectHud emit(StartButton 뒤 `:2595` 직후), BindMenuButtons(`:3158` 뒤), prop 선언부
|
||||||
|
|
||||||
|
- [ ] **Step 1:** StartButton emit(2582-2595) 직후에 BackButton emit 추가(StartButton 패턴 복제, 좌상단 배치)
|
||||||
|
```js
|
||||||
|
select.push(entity({
|
||||||
|
id: guid('menu', 230),
|
||||||
|
path: '/ui/DefaultGroup/CharacterSelectHud/BackButton',
|
||||||
|
modelId: 'uibutton',
|
||||||
|
entryId: 'UIButton',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||||
|
displayOrder: 22,
|
||||||
|
components: [
|
||||||
|
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 56 }, pos: { x: -800, y: 430 }, align: ALIGN_CENTER }),
|
||||||
|
sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }),
|
||||||
|
button(),
|
||||||
|
text({ value: '← 뒤로', fontSize: 26, bold: true, color: GOLD, alignment: 0 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** BindMenuButtons(StartGameHandler 블록 `:3151-3158` 뒤)에 BackButton 바인딩 추가
|
||||||
|
```lua
|
||||||
|
local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton")
|
||||||
|
if charBack ~= nil and charBack.ButtonComponent ~= nil then
|
||||||
|
if self.CharBackHandler ~= nil then
|
||||||
|
charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler)
|
||||||
|
self.CharBackHandler = nil
|
||||||
|
end
|
||||||
|
self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
(이 Lua는 BindMenuButtons 메서드 본문 문자열 끝에 삽입. 실제 탭/`\t` 스타일은 해당 메서드 본문 규칙을 따른다 — BindMenuButtons는 실탭 사용.)
|
||||||
|
|
||||||
|
- [ ] **Step 3:** prop `CharBackHandler` 선언 추가. 기존 핸들러 prop 목록(예: `StartGameHandler`/`NewGameHandler` 등 `prop('any','...')` 선언부)을 grep으로 찾아 같은 형식으로 `CharBackHandler` 추가.
|
||||||
|
```
|
||||||
|
grep -n "StartGameHandler" tools/deck/gen-slaydeck.mjs # prop 선언 위치 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4:** 생성 + 검증
|
||||||
|
```
|
||||||
|
node tools/deck/gen-slaydeck.mjs
|
||||||
|
node tools/verify/count.mjs ui "CharacterSelectHud/BackButton" # 1
|
||||||
|
grep -c "CharBackHandler" RootDesk/MyDesk/SlayDeckController.codeblock # ≥2 (선언+바인딩+해제)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 산출물 재생성 커밋 + .sprite 커밋 + 플레이테스트
|
||||||
|
|
||||||
|
**Files:** `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`(재생성), `RootDesk/MyDesk/*.sprite`(임포트)
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 최종 재생성 + git status로 의도 외 변경 없는지 확인
|
||||||
|
```
|
||||||
|
node tools/deck/gen-slaydeck.mjs
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
Expected: 변경 = gen-slaydeck.mjs, data/characters.json, ui/DefaultGroup.ui, SlayDeckController.codeblock (+ common.gamelogic은 churn이면 내용 동일 시 git checkout 복원). untracked = 임포트 .sprite.
|
||||||
|
|
||||||
|
- [ ] **Step 2:** 소스 커밋(생성기+데이터) → 산출물 커밋(재생성 명시) → .sprite 커밋 분리
|
||||||
|
```
|
||||||
|
git add tools/deck/gen-slaydeck.mjs data/characters.json
|
||||||
|
git commit -m "feat(charselect): 직업 카드 캐릭터 이미지 + 뒤로가기 (소스)"
|
||||||
|
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock
|
||||||
|
git commit -m "chore: 산출물 재생성 (charselect 이미지+뒤로가기)"
|
||||||
|
git add "RootDesk/MyDesk/warrior.sprite" "RootDesk/MyDesk/mage.sprite" "RootDesk/MyDesk/bandit.sprite"
|
||||||
|
git commit -m "chore(assets): 캐릭터 초상화 스프라이트 임포트(전사/법사/도적)"
|
||||||
|
```
|
||||||
|
(2차전직 아트 12종 .sprite는 별도 — 향후 2차 전직 선택 이미지용. 사용자 의사 확인 후 커밋/보류.)
|
||||||
|
|
||||||
|
- [ ] **Step 3:** 메이커 플레이테스트(사용자 워크스페이스 reload 후): 로비 NPC→직업 선택 진입→3 카드에 캐릭터 이미지 표시→클릭 시 금색 테두리·Status 갱신→시작 시 그 직업으로 런→뒤로가기 시 로비 복귀. 빌드 콘솔 0 에러.
|
||||||
|
- 이미지 비율 왜곡/잘림 보이면 Art size(258×318) 조정.
|
||||||
|
- 뒤로가기 시 재텔레포트 jolt 보이면 BackButton 바인딩을 `self:ShowState("lobby")`로 축소.
|
||||||
|
|
||||||
|
- [ ] **Step 4:** push + PR (`node tools/git/gitea-pr.mjs create <spec.json>`, UTF-8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- **스펙 커버리지**: 이미지 적용(T1,T2) · 선택→진행 연결(기존 SelectClass/StartNewGame 불변, T2가 클릭경로 보존) · 선택 금색 테두리(T3) · 뒤로가기→로비(T4) · characters.json 단일소스(T1) · 검증/플레이테스트(T5). 누락 없음.
|
||||||
|
- **플레이스홀더**: RUID·좌표·색·Lua 전부 구체값. count.mjs 부재 시 grep 대체 명시.
|
||||||
|
- **타입 일관성**: `CHARS.portraits[classId]`(classId=warrior/magician/bandit, classCards.classId와 일치). 핸들러 `CharBackHandler` 일관. Art/NameBanner guid(200+i/210+i/230) 미사용 번호.
|
||||||
|
- **리스크**: 이미지 비율(T5 Step3 조정), ShowLobby 재텔레포트(T5 Step3 폴백 ShowState), 메이커 reload 필수(산출물 디스크 반영).
|
||||||
Reference in New Issue
Block a user