Files
maplecontest/docs/superpowers/plans/2026-06-08-deck-controller-fixes.md
2026-06-08 01:08:40 +09:00

12 KiB

덱 컨트롤러 코드리뷰 수정 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 코드리뷰 6건(①self바인딩 ②Card5통일 ③카드클릭=사용 ④카드데이터단일화 ⑤매직넘버 ⑥pcall)을 tools/gen-slaydeck.mjs에서 수정·재생성한다.

Architecture: 모든 산출물(카드 UI·DeckHud·SlayDeckController.codeblock·common.gamelogic)을 생성하는 tools/gen-slaydeck.mjs 단일 소스를 수정하고 재실행한다. DRY는 카드 정의를 codeblock의 self.Cards 테이블 프로퍼티로 단일화하고, 카드 클릭은 카드 엔티티에 ButtonComponent를 추가한 뒤 PlayCard(slot) 메서드를 클로저로 연결해 구현한다.

Tech Stack: Node.js 생성기, MSW codeblock(MapleScript/Lua), msw-maker-mcp(검증).


File Structure

  • Modify: tools/gen-slaydeck.mjs — 모든 수정의 단일 소스.
  • 재생성(출력): ui/DefaultGroup.ui, RootDesk/MyDesk/SlayDeckController.codeblock, Global/common.gamelogic.

기준: codeblock 메서드는 method('Name', , [args])로 정의되고 끝에서 전부 ExecSpace=6로 설정됨. 카드 엔티티(Card1~5)는 upsertUi의 루프가 스타일링함. button() 헬퍼 존재.


Task 1: 생성기 수정 (① ③ ④ ⑥ + ⑤ 일부)

Files: Modify tools/gen-slaydeck.mjs

  • Step 1: 카드에 ButtonComponent + raycast 추가 (③ 클릭 가능)

upsertUi의 카드 루프에서 sp.Color = cards[i - 1].tint; 줄 바로 다음에 아래를 추가:

    sp.RaycastTarget = true;
    const comps = card.jsonString['@components'];
    if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) {
      comps.push(button());
    }
    if (!card.componentNames.includes('MOD.Core.ButtonComponent')) {
      card.componentNames += ',MOD.Core.ButtonComponent';
    }
  • Step 2: Cards 프로퍼티 추가 (④ 단일화 준비)

writeCodeblocks의 properties 배열(prop('any', 'EndTurnHandler') 가 있는 배열)에 항목 추가:

    prop('any', 'Cards'),
  • Step 3: StartCombat 메서드 교체 (④ 카드 테이블 정의)

method('StartCombat', ...) 의 Lua 본문을 아래로 교체:

self.MaxEnergy = 3
self.Turn = 0
self.DiscardPile = {}
self.Hand = {}
self.Cards = {
	Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack" },
	Defend = { name = "방어", cost = 1, desc = "방어도 5", kind = "Skill" },
	Bash = { name = "강타", cost = 2, desc = "피해 10", kind = "Attack" },
}
self.DrawPile = { "Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash" }
self:Shuffle(self.DrawPile)
self:BindButtons()
self:StartPlayerTurn()
  • Step 4: BindButtons 교체 (① 클로저 + ③ 카드 클릭 바인딩)

method('BindButtons', ...) 의 Lua 본문을 아래로 교체:

local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton")
if endTurn ~= nil and endTurn.ButtonComponent ~= nil then
	if self.EndTurnHandler ~= nil then
		endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)
		self.EndTurnHandler = nil
	end
	self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)
end
for i = 1, 5 do
	local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
	if cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then
		cardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)
	end
end
  • Step 5: ApplyCardVisual 교체 (④ self.Cards 사용 + ⑥ pcall 제거)

method('ApplyCardVisual', ...) 의 Lua 본문을 아래로 교체(인자 slot, cardId 유지):

local c = self.Cards[cardId]
if c == nil then
	c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
end
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Cost", tostring(c.cost))
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Name", c.name)
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Desc", c.desc)
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then
	if c.kind == "Attack" then
		cardEntity.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
	elseif c.kind == "Skill" then
		cardEntity.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
	else
		cardEntity.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
	end
end
  • Step 6: PlayCard + Toast 메서드 추가 (③)

method('AnimateCardFrom', ...) 항목 다음(메서드 배열 안)에 두 메서드를 추가:

    method('PlayCard', `if self.Hand == nil then
	return
end
local cardId = self.Hand[slot]
if cardId == nil then
	return
end
local c = self.Cards[cardId]
if c == nil then
	return
end
if self.Energy < c.cost then
	self:Toast("에너지가 부족합니다")
	return
end
self.Energy = self.Energy - c.cost
self:Toast(c.name .. " — " .. c.desc)
table.remove(self.Hand, slot)
table.insert(self.DiscardPile, cardId)
self:RenderHand(false)
self:RenderPiles()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
    method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),

(⑤: 손패/슬롯 수 5는 UI 카드 엔티티가 정확히 5개라 고정값으로 둠 — 별도 상수 불필요. 시작 에너지/MaxEnergy는 이미 프로퍼티.)

  • Step 7: 구문 확인 + 커밋
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node --check tools/gen-slaydeck.mjs
git add tools/gen-slaydeck.mjs
git commit -m "덱 컨트롤러 생성기: 핸들러 클로저화·카드데이터 단일화·카드클릭 사용·pcall 제거"

Expected: node --check 무출력(exit 0).


Task 2: 재생성 + 데이터 검증

Files: Modify ui/DefaultGroup.ui, RootDesk/MyDesk/SlayDeckController.codeblock, Global/common.gamelogic

  • Step 1: 재생성
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node tools/gen-slaydeck.mjs

Expected: Slay deck UI and combat codeblocks generated.

  • Step 2: codeblock 검증
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));const ms=j.ContentProto.Json.Methods;const names=ms.map(m=>m.Name);console.log('has PlayCard:',names.includes('PlayCard'));console.log('has Toast:',names.includes('Toast'));const bind=ms.find(m=>m.Name==='BindButtons').Code;console.log('endturn closure:',bind.includes('function() self:EndPlayerTurn() end'));console.log('card click bind:',bind.includes('function() self:PlayCard(i) end'));const av=ms.find(m=>m.Name==='ApplyCardVisual').Code;console.log('no pcall:',!av.includes('pcall'));console.log('uses self.Cards:',av.includes('self.Cards[cardId]'));const sc=ms.find(m=>m.Name==='StartCombat').Code;console.log('Cards table:',sc.includes('self.Cards ='))"

Expected: 모두 true.

  • Step 3: UI 검증 (카드 버튼 + Card5 통일)
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node -e "const j=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const E=j.ContentProto.Entities;let okBtn=true,okImg=true;for(let i=1;i<=5;i++){const c=E.find(e=>e.path==='/ui/DefaultGroup/CardHand/Card'+i);if(!c){okBtn=false;continue;}if(!(c.componentNames||'').includes('MOD.Core.ButtonComponent'))okBtn=false;const sp=c.jsonString['@components'].find(x=>x['@type']==='MOD.Core.SpriteGUIRendererComponent');if(sp.ImageRUID.DataId!=='')okImg=false;}const c5=E.find(e=>e.path==='/ui/DefaultGroup/CardHand/Card5');const hasDesc=E.some(e=>e.path==='/ui/DefaultGroup/CardHand/Card5/Desc');console.log('all cards have Button:',okBtn);console.log('all cards no image (uniform):',okImg);console.log('Card5 has Desc child:',hasDesc)"

Expected: all cards have Button: true, all cards no image (uniform): true, Card5 has Desc child: true.

  • Step 4: JSON 유효성 + 커밋
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node -e "JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));JSON.parse(require('fs').readFileSync('Global/common.gamelogic','utf8'));console.log('JSON ok')"
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock Global/common.gamelogic
git commit -m "재생성: 카드 클릭 사용·균일 카드·핸들러 수정 반영"

Expected: JSON ok.


Task 3: Maker Play 검증 (컨트롤러)

Files: 없음

  • Step 1: reload: maker_refresh_workspace.
  • Step 2: 시작 맵 활성화 확인: maker_get_current_map. (어느 맵이든 카드 UI는 전역이라 표시됨)
  • Step 3: play: maker_play.
  • Step 4: 클릭 시뮬레이션 + 상태 확인: maker_execute_script(client)로 PlayCard 직접 호출해 동작 확인:
    local ctrl = _EntityService:GetEntityByPath("/common")
    -- 초기 상태
    local c = ctrl.SlayDeckController
    log("BEFORE energy="..tostring(c.Energy).." hand="..tostring(#c.Hand).." discard="..tostring(#c.DiscardPile))
    c:PlayCard(1)
    log("AFTER energy="..tostring(c.Energy).." hand="..tostring(#c.Hand).." discard="..tostring(#c.DiscardPile))
    
    maker_logs(normal)에서 카드 사용 후 energy 감소·hand 감소·discard 증가 확인. (또는 maker_mouse_input으로 카드 클릭)
  • Step 5: screenshot: maker_screenshot → Read로 5장 균일·DeckHud(에너지/덱 카운트) 확인.
  • Step 6: stop: maker_stop.

문제 시: 핸들러 self·PlayCard 동작 로그로 진단 후 Task 1 수정·재생성.


Task 4: stash 복구 + 무결성 검증

Files: map/map02.map, map/map05.map, map/map06.map, map/map07.map, map/map10.map, map/map11.map (복구 대상)

  • Step 1: stash 적용
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
git stash list
git stash apply 2>&1 | head -20

(충돌 시 해당 파일은 main 버전 유지하고 stash 변경만 수동 반영하거나, 무의미하면 제외 — 아래 검증으로 판단)

  • Step 2: 무결성 검증 (몬스터/타일셋 유지 확인)
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node -e "const old=['8ef238e0d0ca4bb783aca526cff35d11','6c7130f51a654803a1c39cbe30e2f427','3e76c89ae8e7477ca871f5bbcd6f6f29','6d381bea1bcb4504b518a1fbfa0904ac','c96c11f9a3f845a4b6a27d9ca10ab103'];for(const t of ['02','05','06','07','10','11']){const j=JSON.parse(require('fs').readFileSync('map/map'+t+'.map','utf8'));const E=j.ContentProto.Entities;const ms=E.filter(e=>(e.componentNames||'').includes('script.Monster'));const sprs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.SpriteRendererComponent').SpriteRUID);const okNoOld=sprs.every(s=>!old.includes(s));const ts=E.find(e=>(e.path||'').endsWith('/TileMap')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.TileMapComponent').TileSetRUID.DataId;console.log('map'+t,'monsters='+ms.length,'noOldSprite='+okNoOld,'tileset='+(ts!=='9dfea3808bbd49a5877d8624df21b1c7'))}"

Expected: 각 맵 monsters=2, noOldSprite=true, tileset=true. (= 몬스터/타일셋 작업 유지됨)

  • Step 3: 판정 및 커밋

  • 무결성 OK → 복구분 커밋:

    git add map/map02.map map/map05.map map/map06.map map/map07.map map/map10.map map/map11.map
    git commit -m "Maker 세션 재저장분(맵 02/05/06/07/10/11) 복구 포함"
    git stash drop
    
  • 무결성 실패(작업 되돌려짐/손상) → 복구 취소하고 사용자에게 보고:

    git checkout -- map/map02.map map/map05.map map/map06.map map/map07.map map/map10.map map/map11.map
    

    (stash는 보존)


검증 요약

  • 생성기 node --check 통과
  • codeblock: PlayCard/Toast 존재, EndTurn·카드클릭 클로저, self.Cards 사용, pcall 없음
  • UI: Card1~5 ButtonComponent+raycast, 5장 균일(이미지 없음·Desc 존재)
  • Maker Play: PlayCard 호출 시 energy↓·hand↓·discard↑, 5장 균일 렌더
  • stash 복구분 무결성(몬스터2·old미사용·타일셋교체) 검증 후 포함