에너지 오브/턴종료 재배치 → TopBar → 플레이어 패널+SetHpBar(width) → 타겟 프레임·가독성 → ShowState → 재생성·겹침 정적검사 → 플레이테스트. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 KiB
전투 화면 UI/HUD 전면 정비 (P1) 구현 계획
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: 전투 화면을 STS2 배치로 재구성해 UI 겹침 0·시각 위계 확립 (기능 불변, 레이아웃·표시 로직만).
Architecture: 모든 UI/컨트롤러는 tools/deck/gen-slaydeck.mjs 단일 소스에서 생성(직접 편집 금지, 루트에서 node tools/deck/gen-slaydeck.mjs). UI 엔티티는 upsertUi()의 entity()/transform()/sprite()/text()/button() 헬퍼, 컨트롤러 Lua는 method(Name, Code, Args?) 템플릿 문자열(${...}=JS 보간). 좌표계: 부모 중심 원점, anchoredPosition.
Tech Stack: Node.js ESM 생성기, MSW Lua codeblock, JSON 산출물.
배경 (구현자용)
- 화면 1920×1080 중심좌표(±960, ±540).
DeckHud=하단 1280×330(y180 bottom-center),CardHand=카드 5장(y180),CombatHud=전체 1920×1080. - 확인된 겹침:
DeckHud/EndTurnButton(0,135,170×58) ↔DeckHud/Energy(0,90,220×42) 5px;AllDeckButton(470,135)이 버린덱(590,8,132×186)과 5px 간격. - 기존 가시성:
HideGameHud(전투 HUD 일괄 off)가 이미 존재(사용자 PR) — ShowState는 이를 재사용해 확장. - guid 네임스페이스:
guid('cmb', N)— 기존 사용 대역: 010(순차 cmbN), 41144(슬롯 6종×4). 신규는 200+ 사용(TopBar 200209, PlayerPanel 210219, TargetFrame 221~224). - 생성기 실행 검증은 매 Task:
node --check+node tools/deck/gen-slaydeck.mjs성공 + 해당 확인 스크립트. 산출물 커밋은 Task 6에서 일괄(중간 Task는 소스만 커밋하고 산출물은git checkout -- ui/DefaultGroup.ui Global/common.gamelogic RootDesk/MyDesk/SlayDeckController.codeblock로 복원).
파일 구조
| 파일 | 책임 |
|---|---|
tools/deck/gen-slaydeck.mjs |
전 변경(UI 좌표·신규 엔티티·컨트롤러 표시 로직) |
| 산출물 3종 | Task 6에서 재생성·커밋 |
Task 1: 하단 HUD — 에너지 오브(좌)·턴 종료(우)
Files: Modify tools/deck/gen-slaydeck.mjs
- Step 1: Energy 텍스트 엔티티를 EnergyOrb 패널로 교체
upsertUi()에서 path: '/ui/DefaultGroup/DeckHud/Energy' 엔티티 push 블록(add(entity({ ... '에너지 3/3' ... }))) 전체를 삭제하고, 그 자리에:
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EnergyOrb',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 3,
components: [
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 130 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }),
],
}));
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }),
sprite({ color: TRANSPARENT }),
text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }),
],
}));
add(entity({
id: guid('hud', hud.length),
path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }),
sprite({ color: TRANSPARENT }),
text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }),
],
}));
- Step 2: EndTurnButton 이동·확대
path: '/ui/DefaultGroup/DeckHud/EndTurnButton' 엔티티의 transform 줄을
transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 130 }, align: ALIGN_CENTER }),
로 교체하고, 같은 엔티티의 text({ value: '턴 종료', fontSize: 25, ... 를 fontSize: 28, 로.
- Step 3: RenderPiles 에너지 경로/포맷 갱신
method('RenderPiles', ...) 안의
self:SetText("/ui/DefaultGroup/DeckHud/Energy", "에너지 " .. string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
를
self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy))
로 교체.
-
Step 4: 검증 —
node --check tools/deck/gen-slaydeck.mjs후 실행:node tools/deck/gen-slaydeck.mjs && node -e "const ui=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const p=ui.ContentProto.Entities.map(e=>e.path);console.log('orb:',p.includes('/ui/DefaultGroup/DeckHud/EnergyOrb'),'| oldEnergy gone:',!p.includes('/ui/DefaultGroup/DeckHud/Energy'))"Expected:orb: true | oldEnergy gone: true. 그 후 산출물 복원:git checkout -- ui/DefaultGroup.ui Global/common.gamelogic RootDesk/MyDesk/SlayDeckController.codeblock -
Step 5: Commit —
git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(combat-ui): 에너지 오브(좌)·턴 종료 버튼(우) 재배치"
Task 2: 상단 TopBar (막·골드·유물·모든덱보기 통합)
Files: Modify tools/deck/gen-slaydeck.mjs
- Step 1: 기존 Floor/Gold/Relics 엔티티 제거
upsertUi() CombatHud 빌드에서 ['Floor', { x: -820, y: 480 }, ...]/['Gold', { x: 820, y: 480 }, ...] 루프 블록과 path: '/ui/DefaultGroup/CombatHud/Relics' push 블록을 삭제.
- Step 2: DeckHud의 AllDeckButton 엔티티 제거
path: '/ui/DefaultGroup/DeckHud/AllDeckButton' push 블록(레이블 텍스트 포함 엔티티 1개) 삭제.
- Step 3: CombatHud에 TopBar 추가 (Result push 이전 위치에):
combat.push(entity({
id: guid('cmb', 200),
path: '/ui/DefaultGroup/CombatHud/TopBar',
modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 9,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }),
],
}));
const topTexts = [
['Floor', -520, 160, '막 1/3', GOLD],
['Gold', -360, 160, '골드 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
['Relics', 60, 560, '유물: 없음', { r: 0.8, g: 0.7, b: 0.95, a: 1 }],
];
topTexts.forEach(([suffix, x, w, value, color], ti) => {
combat.push(entity({
id: guid('cmb', 201 + ti),
path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`,
modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: ti,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value, fontSize: suffix === 'Relics' ? 18 : 22, bold: true, color, alignment: 4 }),
],
}));
});
combat.push(entity({
id: guid('cmb', 205),
path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton',
modelId: 'uibutton', entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 150, y: 40 }, pos: { x: 510, y: 0 } }),
sprite({ color: DARK, type: 1, raycast: true }),
button(),
text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }),
],
}));
-
Step 4: 컨트롤러 경로 갱신 (정확 치환 3건)
-
RenderRun:"/ui/DefaultGroup/CombatHud/Floor"→"/ui/DefaultGroup/CombatHud/TopBar/Floor","/ui/DefaultGroup/CombatHud/Gold"→"/ui/DefaultGroup/CombatHud/TopBar/Gold" -
RenderRelics(끝부분 SetText):"/ui/DefaultGroup/CombatHud/Relics"→"/ui/DefaultGroup/CombatHud/TopBar/Relics" -
BindButtons:"/ui/DefaultGroup/DeckHud/AllDeckButton"→"/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton" -
Step 5: 검증 —
node --check후 실행:node tools/deck/gen-slaydeck.mjs && node -e "const fs=require('fs');const ui=JSON.parse(fs.readFileSync('ui/DefaultGroup.ui','utf8'));const p=ui.ContentProto.Entities.map(e=>e.path);console.log('topbar:',['/TopBar','/TopBar/Floor','/TopBar/Gold','/TopBar/Relics','/TopBar/AllDeckButton'].every(s=>p.includes('/ui/DefaultGroup/CombatHud'+s)),'| old gone:',!p.includes('/ui/DefaultGroup/CombatHud/Floor')&&!p.includes('/ui/DefaultGroup/CombatHud/Relics')&&!p.includes('/ui/DefaultGroup/DeckHud/AllDeckButton'));const cb=fs.readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8');console.log('paths updated:',cb.includes('TopBar/Floor')&&cb.includes('TopBar/Relics')&&cb.includes('TopBar/AllDeckButton')&&!cb.includes('DeckHud/AllDeckButton'))"Expected: 모두 true. 산출물 복원(Task 1과 동일 명령). -
Step 6: Commit —
git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(combat-ui): 상단 TopBar (막·골드·유물·모든덱보기 통합)"
Task 3: 플레이어 패널 + SetHpBar 폭 인자
Files: Modify tools/deck/gen-slaydeck.mjs
- Step 1: SetHpBar에 width 인자 추가
method('SetHpBar', ...) 전체를 다음으로 교체:
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
if e == nil or e.UITransformComponent == nil then
return
end
local ratio = 0
if maxHp > 0 then ratio = hp / maxHp end
if ratio < 0 then ratio = 0 end
local w = width * ratio
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
]),
그리고 RenderCombat 안의 기존 호출 self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp) 를 self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W}) 로 교체.
- Step 2: PlayerBg/PlayerHp/PlayerBlock 엔티티 제거 → PlayerPanel 추가
upsertUi()에서 path: '/ui/DefaultGroup/CombatHud/PlayerBg' push 블록과 playerTexts 배열+루프를 삭제하고, 그 자리에:
const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel';
combat.push(entity({
id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 5,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -480 }, align: ALIGN_CENTER }),
sprite({ color: PANEL_BG, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }),
sprite({ color: TRANSPARENT }),
text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }),
],
}));
combat.push(entity({
id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 1,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }),
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 2,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: -94, y: -6 } }),
sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }),
],
}));
combat.push(entity({
id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 3,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }),
sprite({ color: TRANSPARENT }),
text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
const blockBadge = entity({
id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }),
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
],
});
blockBadge.jsonString.enable = false;
combat.push(blockBadge);
combat.push(entity({
id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
- Step 3: RenderCombat 플레이어부 교체
RenderCombat 끝의
self:SetText("/ui/DefaultGroup/CombatHud/PlayerHp", "HP " .. string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
self:SetText("/ui/DefaultGroup/CombatHud/PlayerBlock", "방어 " .. string.format("%d", self.PlayerBlock))
를
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
로 교체.
-
Step 4: 검증 —
node --check후 실행+확인:node tools/deck/gen-slaydeck.mjs && node -e "const fs=require('fs');const ui=JSON.parse(fs.readFileSync('ui/DefaultGroup.ui','utf8'));const p=ui.ContentProto.Entities.map(e=>e.path);console.log('panel:',p.includes('/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill'),'| old gone:',!p.includes('/ui/DefaultGroup/CombatHud/PlayerHp'));const cb=JSON.parse(fs.readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));const m=cb.ContentProto.Json.Methods.find(x=>x.Name==='SetHpBar');console.log('width arg:',m.Arguments.length===4)"Expected: 모두 true. 산출물 복원. -
Step 5: Commit —
git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(combat-ui): 플레이어 패널(HP바·방어 뱃지) + SetHpBar 폭 인자"
Task 4: 타겟 프레임 + 몬스터 슬롯 가독성 + 의도 색상
Files: Modify tools/deck/gen-slaydeck.mjs
- Step 1: 슬롯 루프에 TargetFrame 추가 + 가독성 조정
upsertUi() 몬스터 슬롯 루프(for (let i = 1; i <= MAX_MONSTERS; i++))에서:
- 슬롯 컨테이너 push 직후, Name push 이전에 추가:
const targetFrame = entity({
id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }),
sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }),
],
});
targetFrame.jsonString.enable = false;
combat.push(targetFrame);
- Name/Hp/HpBarBg/HpBarFill/Intent의
displayOrder를 각각 1/2/3/4/5로 +1. - Name
fontSize: 20→22, HpfontSize: 18→20. - 파일 상단
const HP_BAR_W = 120;→const HP_BAR_W = 140;(몬스터 바 폭 확대 — HpBarBg/Fill·RenderCombat 보간이 모두 이 상수 사용).
- Step 2: RenderCombat 몬스터부 — [타겟] 제거·TargetFrame·의도 색상
RenderCombat의 몬스터 루프 본문에서
if i == self.TargetIndex then t = "[타겟] " .. t end
self:SetText(base .. "/Intent", t)
를
self:SetText(base .. "/Intent", t)
self:SetEntityEnabled(base .. "/TargetFrame", i == self.TargetIndex)
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
if intent.kind == "Attack" then
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
else
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
end
end
로 교체.
-
Step 3: 검증 —
node --check후 실행+확인:node tools/deck/gen-slaydeck.mjs && node -e "const fs=require('fs');const ui=JSON.parse(fs.readFileSync('ui/DefaultGroup.ui','utf8'));const tf=ui.ContentProto.Entities.filter(e=>e.path.endsWith('/TargetFrame')).length;const cb=fs.readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8');console.log('frames:',tf,'| no [타겟]:',!cb.includes('[타겟]'),'| color:',cb.includes('FontColor = Color(1, 0.45'))"Expected:frames: 4 | no [타겟]: true | color: true. 산출물 복원. -
Step 4: Commit —
git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(combat-ui): 타겟 프레임·몬스터 슬롯 가독성·의도 색상"
Task 5: ShowState 가시성 통일 + Result 정리
Files: Modify tools/deck/gen-slaydeck.mjs
- Step 1: ShowState 메서드 추가 (
HideGameHud메서드 바로 다음에):
method('ShowState', `self:HideGameHud()
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect")
if state == "map" then
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true)
elseif state == "combat" then
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
elseif state == "shop" then
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
elseif state == "rest" then
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
- Step 2: 호출부 치환 (각각 정확 치환)
ShowMainMenu:
self:HideGameHud()
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", true)
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", false)
→ self:ShowState("menu")
2. ShowCharacterSelect:
self:HideGameHud()
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", false)
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", true)
→ self:ShowState("charselect")
3. StartNewGame의
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", false)
self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", false)
→ 삭제(StartRun→ShowMap이 ShowState("map")으로 처리).
4. StartCombat 첫 4줄
self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false)
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true)
self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true)
→
self:ShowState("combat")
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
ShowMap첫 3줄(DeckHud/CardHand/CombatHudoff) →self:ShowState("map")(그 아래RenderMap·MapHud enable 블록에서 MapHud enable 부분은 중복되지만 무해 — 기존local hud = ...MapHud... hud.Enable = true블록은 삭제).ShowShop끝의 ShopHud enable 블록(local hud = ...ShopHud ... end) →self:ShowState("shop")(RenderShop 호출은 유지).ShowRest끝의 RestHud enable 블록 →self:ShowState("rest")(텍스트·RenderCombat 호출 유지).LeaveNode는 기존 그대로(ShowMap 경유).
-
Step 3: 검증 —
node --check후 실행+확인:node tools/deck/gen-slaydeck.mjs && node -e "const cb=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));const names=cb.ContentProto.Json.Methods.map(m=>m.Name);const s=JSON.stringify(cb);console.log('ShowState:',names.includes('ShowState'),'| StartCombat resets Result:',/ShowState\(\\\"combat\\\"\)[\s\S]{0,200}Result/.test(s))"Expected: 둘 다 true. 산출물 복원. -
Step 4: Commit —
git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(combat-ui): ShowState 가시성 통일 + 전투 시작 시 Result 초기화"
Task 6: 재생성 · 겹침 정적 검사 · 산출물 커밋
Files: 산출물 3종
-
Step 1: 재생성 —
node tools/deck/gen-slaydeck.mjs(exit 0) -
Step 2: JSON·중복 id·결정성
node -e "const fs=require('fs');for(const f of ['ui/DefaultGroup.ui','Global/common.gamelogic','RootDesk/MyDesk/SlayDeckController.codeblock'])JSON.parse(fs.readFileSync(f,'utf8'));const ui=JSON.parse(fs.readFileSync('ui/DefaultGroup.ui','utf8'));const ids=ui.ContentProto.Entities.map(e=>e.id);console.log('dup:',ids.filter((x,i)=>ids.indexOf(x)!==i).length)"→dup: 0그리고git add -A후node tools/deck/gen-slaydeck.mjs재실행 →git diff --stat비어있음(결정적). -
Step 3: 하단 HUD 겹침 정적 검사 (AABB 페어와이즈):
node -e "const ui=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const E=ui.ContentProto.Entities;const get=p=>E.find(e=>e.path===p);const box=p=>{const e=get(p);const t=e.jsonString['@components'].find(c=>c['@type']==='MOD.Core.UITransformComponent');return {p,x:t.anchoredPosition.x,y:t.anchoredPosition.y,w:t.RectSize.x,h:t.RectSize.y};};const items=['/ui/DefaultGroup/DeckHud/EnergyOrb','/ui/DefaultGroup/DeckHud/EndTurnButton','/ui/DefaultGroup/DeckHud/DrawPile','/ui/DefaultGroup/DeckHud/DiscardPile'].map(box);const hit=(a,b)=>Math.abs(a.x-b.x)*2<(a.w+b.w)&&Math.abs(a.y-b.y)*2<(a.h+b.h);let bad=0;for(let i=0;i<items.length;i++)for(let j=i+1;j<items.length;j++)if(hit(items[i],items[j])){bad++;console.log('OVERLAP',items[i].p,items[j].p);}console.log(bad===0?'no overlap ✓':'OVERLAPS:'+bad)"Expected:no overlap ✓ -
Step 4: sim 회귀 —
node --test tools/balance/sim-balance.test.mjs→ 14/14 -
Step 5: Commit —
git add ui/DefaultGroup.ui Global/common.gamelogic RootDesk/MyDesk/SlayDeckController.codeblock && git commit -m "feat(combat-ui): UI 정비 산출물 재생성"
Task 7: 메이커 플레이테스트 (수동/MCP — 컨트롤러 주도)
maker MCP: refresh → build log 0 → play. execute_script로 상태 전환하며 스크린샷:
- 초기(menu): 메인메뉴만, 전투 HUD 비침 없음
s:ShowCharacterSelect()→ 캐릭터선택만s.SelectedClass="warrior"; s:StartNewGame()→ 맵(MapHud만)s:PickNode("A")→ 전투: TopBar(막/골드/유물/모든덱보기)·에너지 오브(좌)·턴종료(우)·플레이어 패널(HP바)·타겟 프레임(1번 슬롯 골드 하이라이트)s:SetTarget(2)→ 프레임 이동 확인,s:PlayCard(<방어 슬롯>)→ 방어 뱃지 표시- 전체 처치 → 보상 →
s:PickReward(1)→ 맵 복귀 - 상점(D)·휴식(C) 화면
겹침·비침 발견 시 좌표 조정 → 재생성 → reload → 재확인 → 산출물 커밋(
fix(combat-ui): 플레이테스트 좌표 튜닝).
Self-Review 결과
- 스펙 커버리지: §3.1→T1, §3.2→T2, §3.3→T3, §3.4→T4, §3.5→T5(HideGameHud 재사용으로 구현 — 스펙 의도 동일), §3.6→T7에서 확인(채팅 숨김 API는 비차단), 검증§6→T6·T7. 전 항목 매핑.
- 플레이스홀더 없음: 모든 단계 실제 코드/명령 포함.
- 타입/이름 일관성:
EnergyOrb/Value·TopBar/{Floor,Gold,Relics,AllDeckButton}·PlayerPanel/{Name,HpBarBg,HpBarFill,HpText,BlockBadge/Value}·TargetFrame·SetHpBar(path,hp,maxHp,width)·ShowState(state)— Task 간 일치. guid 대역 200224 기존(010·41~144)과 비충돌. - 주의: T2 Step 1에서 Floor/Gold 루프 제거 시 그 루프가 쓰던
cmbN증가가 사라져 후속 Relics/Result id가 변함 — 전부 재생성이라 무해. T5의 ShowMap 치환 시 기존 MapHud enable 블록 삭제 누락하면 중복(무해하나 정리).