10 KiB
직업 선택 캐릭터 이미지 + 뒤로가기 — 구현 계획
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작성
{
"portraits": {
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
"magician": "3b9ea1f066a744bb859df47fef817277",
"bandit": "efa920e58d31426486ef974106e7dc8b"
}
}
- Step 2: gen-slaydeck.mjs NODEICONS 검증 블록(
:96) 바로 뒤에 로드+fail-fast 검증 추가
// 캐릭터 선택 초상화 (메이커 임포트 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 변경:
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로 올림.
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 패턴 복제, 좌상단 배치)
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 바인딩 추가
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 필수(산출물 디스크 반영).