Files
maplecontest/docs/superpowers/plans/2026-06-16-charselect-images-back.md

10 KiB
Raw Permalink Blame History

직업 선택 캐릭터 이미지 + 뒤로가기 — 구현 계획

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) 위치를 하단으로 — transformpos: { 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: 0displayOrder: 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/NewGameHandlerprop('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 필수(산출물 디스크 반영).