docs(B): 카드 전투 통합 설계·구현 계획 문서 추가
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
481
docs/superpowers/plans/2026-06-08-card-combat-integration.md
Normal file
481
docs/superpowers/plans/2026-06-08-card-combat-integration.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# 카드 전투 통합 (TODO B) 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:** 카드 사용이 실제 적 HP·플레이어 Block·적 의도·승패에 반영되는 단일 전투 루프를 완성한다.
|
||||
|
||||
**Architecture:** 모든 변경은 `tools/gen-slaydeck.mjs` 단일 생성기에서 만든다. 적/플레이어 전투 상태는 `SlayDeckController` codeblock 내부 속성으로 보유(필드 `Monster.codeblock`과 분리). UI는 `CombatHud` 그룹으로 DeckHud와 별도 생성. 수치(플레이어 80 / 적 45 / 의도 10·6·방8)는 임시 placeholder.
|
||||
|
||||
**Tech Stack:** Node.js ESM 생성기(`gen-slaydeck.mjs`), MSW Lua codeblock, MSW UI JSON. 검증은 `node --check` + 재생성 + sha1 결정성 + 메이커 Play.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `tools/gen-slaydeck.mjs` — 유일한 변경 대상.
|
||||
- `upsertUi()`: `CombatHud` 그룹(적/플레이어 패널·결과 텍스트) 생성 추가, 정리 필터 확장.
|
||||
- `writeCodeblocks()`: `SlayDeckController` 속성·메서드 추가/수정.
|
||||
- 생성물(자동, 직접 편집 금지): `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock`, `Global/common.gamelogic`.
|
||||
|
||||
검증 한계: MSW codeblock Lua는 단위 테스트 러너가 없다. 자동 검증은 생성기 문법·재생성·결정성·JSON 유효성까지, 실제 동작은 메이커 Play(사용자)로 확인.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 카드 데이터 수치화 (Cards 테이블 + UI 카드 배열)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`upsertUi` 내 `cards` 배열, `writeCodeblocks` 내 `StartCombat`의 `self.Cards`)
|
||||
|
||||
- [ ] **Step 1: `upsertUi`의 카드 배열은 표시용 그대로 두되, codeblock `Cards`에 수치 필드 추가**
|
||||
|
||||
`writeCodeblocks()`의 `StartCombat` 메서드 코드에서 `self.Cards` 정의를 아래로 교체:
|
||||
|
||||
```lua
|
||||
self.Cards = {
|
||||
Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack", damage = 6 },
|
||||
Defend = { name = "방어", cost = 1, desc = "방어도 5", kind = "Skill", block = 5 },
|
||||
Bash = { name = "강타", cost = 2, desc = "피해 10", kind = "Attack", damage = 10 },
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음 (출력 없음, exit 0)
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): 카드 데이터에 damage/block 수치 필드 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 전투 상태 속성 + StartCombat 초기화
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`writeCodeblocks` 속성 배열, `StartCombat` 메서드)
|
||||
|
||||
- [ ] **Step 1: codeblock 속성 추가**
|
||||
|
||||
`codeblock('SlayDeckController', ...)`의 properties 배열 끝에 추가:
|
||||
|
||||
```js
|
||||
prop('number', 'PlayerHp', '0'),
|
||||
prop('number', 'PlayerMaxHp', '80'),
|
||||
prop('number', 'PlayerBlock', '0'),
|
||||
prop('number', 'EnemyHp', '0'),
|
||||
prop('number', 'EnemyMaxHp', '45'),
|
||||
prop('number', 'EnemyBlock', '0'),
|
||||
prop('number', 'EnemyIntentIndex', '1'),
|
||||
prop('boolean', 'CombatOver', 'false'),
|
||||
prop('any', 'EnemyIntents'),
|
||||
prop('any', 'EnemyName'),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `StartCombat`에 전투 상태 초기화 추가**
|
||||
|
||||
`StartCombat` 코드의 맨 위(`self.MaxEnergy = 3` 직후)에 삽입:
|
||||
|
||||
```lua
|
||||
self.PlayerMaxHp = 80
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
self.PlayerBlock = 0
|
||||
self.EnemyName = "슬라임"
|
||||
self.EnemyMaxHp = 45
|
||||
self.EnemyHp = self.EnemyMaxHp
|
||||
self.EnemyBlock = 0
|
||||
self.EnemyIntents = {
|
||||
{ kind = "Attack", value = 10 },
|
||||
{ kind = "Attack", value = 6 },
|
||||
{ kind = "Defend", value = 8 },
|
||||
}
|
||||
self.EnemyIntentIndex = 1
|
||||
self.CombatOver = false
|
||||
```
|
||||
|
||||
그리고 `StartCombat` 끝(`self:StartPlayerTurn()` 직전)에 `self:RenderCombat()` 추가.
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): 플레이어/적 전투 상태 속성·초기화 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 전투 헬퍼 메서드 (데미지/적턴/승패/렌더)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`writeCodeblocks` methods 배열에 신규 메서드 추가)
|
||||
|
||||
`SetText`는 엔티티 nil 가드가 있어, 참조하는 UI가 Task 5에서 생성되기 전이어도 안전(no-op).
|
||||
|
||||
- [ ] **Step 1: 신규 메서드들을 methods 배열에 추가 (`Toast` 메서드 정의 뒤)**
|
||||
|
||||
```js
|
||||
method('DealDamageToEnemy', `local dmg = amount
|
||||
if self.EnemyBlock > 0 then
|
||||
local absorbed = math.min(self.EnemyBlock, dmg)
|
||||
self.EnemyBlock = self.EnemyBlock - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
self.EnemyHp = self.EnemyHp - dmg
|
||||
if self.EnemyHp < 0 then
|
||||
self.EnemyHp = 0
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('DealDamageToPlayer', `local dmg = amount
|
||||
if self.PlayerBlock > 0 then
|
||||
local absorbed = math.min(self.PlayerBlock, dmg)
|
||||
self.PlayerBlock = self.PlayerBlock - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
self.PlayerHp = self.PlayerHp - dmg
|
||||
if self.PlayerHp < 0 then
|
||||
self.PlayerHp = 0
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('EnemyTurn', `self.EnemyBlock = 0
|
||||
local intent = self.EnemyIntents[self.EnemyIntentIndex]
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
self:DealDamageToPlayer(intent.value)
|
||||
elseif intent.kind == "Defend" then
|
||||
self.EnemyBlock = self.EnemyBlock + intent.value
|
||||
end
|
||||
end
|
||||
self.EnemyIntentIndex = self.EnemyIntentIndex + 1
|
||||
if self.EnemyIntentIndex > #self.EnemyIntents then
|
||||
self.EnemyIntentIndex = 1
|
||||
end
|
||||
self:RenderCombat()`),
|
||||
method('CheckCombatEnd', `if self.EnemyHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("승리!")
|
||||
-- TODO(E): 전투 보상 훅 — 카드 보상/골드/유물 선택 진입점
|
||||
elseif self.PlayerHp <= 0 then
|
||||
self.CombatOver = true
|
||||
self:ShowResult("패배...")
|
||||
end`),
|
||||
method('ShowResult', `self:SetText("/ui/DefaultGroup/CombatHud/Result", text)
|
||||
local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result")
|
||||
if entity ~= nil then
|
||||
entity.Enable = true
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
|
||||
method('RenderCombat', `self:SetText("/ui/DefaultGroup/CombatHud/EnemyName", self.EnemyName)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyHp", "HP " .. tostring(self.EnemyHp) .. "/" .. tostring(self.EnemyMaxHp))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyBlock", "방어 " .. tostring(self.EnemyBlock))
|
||||
local intent = self.EnemyIntents[self.EnemyIntentIndex]
|
||||
local intentText = ""
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
intentText = "의도: 공격 " .. tostring(intent.value)
|
||||
elseif intent.kind == "Defend" then
|
||||
intentText = "의도: 방어 " .. tostring(intent.value)
|
||||
end
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyIntent", intentText)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerHp", "HP " .. tostring(self.PlayerHp) .. "/" .. tostring(self.PlayerMaxHp))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerBlock", "방어 " .. tostring(self.PlayerBlock))`),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 3: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): 데미지/적턴/승패/전투렌더 헬퍼 메서드 추가"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 턴 흐름 배선 (PlayCard 효과·EndPlayerTurn·StartPlayerTurn)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`StartPlayerTurn`, `EndPlayerTurn`, `PlayCard` 메서드 코드)
|
||||
|
||||
- [ ] **Step 1: `StartPlayerTurn` 교체**
|
||||
|
||||
```lua
|
||||
self.Turn = self.Turn + 1
|
||||
self.Energy = self.MaxEnergy
|
||||
self.PlayerBlock = 0
|
||||
self:DrawCards(5)
|
||||
self:RenderHand(true)
|
||||
self:RenderCombat()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `EndPlayerTurn` 교체**
|
||||
|
||||
```lua
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
for i = 1, #self.Hand do
|
||||
table.insert(self.DiscardPile, self.Hand[i])
|
||||
end
|
||||
self.Hand = {}
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:EnemyTurn()
|
||||
self:CheckCombatEnd()
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `PlayCard` 효과 분기 교체**
|
||||
|
||||
`PlayCard` 코드를 아래로 교체(에너지 차감 후 Toast 대신 효과 적용):
|
||||
|
||||
```lua
|
||||
if self.CombatOver == true then
|
||||
return
|
||||
end
|
||||
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
|
||||
if c.kind == "Attack" then
|
||||
if c.damage ~= nil then
|
||||
self:DealDamageToEnemy(c.damage)
|
||||
end
|
||||
elseif c.kind == "Skill" then
|
||||
if c.block ~= nil then
|
||||
self.PlayerBlock = self.PlayerBlock + c.block
|
||||
end
|
||||
end
|
||||
table.remove(self.Hand, slot)
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): PlayCard 효과 분기·적턴·승패 턴흐름 배선"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: CombatHud UI 엔티티 생성
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/gen-slaydeck.mjs` (`upsertUi`: 정리 필터 확장 + CombatHud 그룹 생성)
|
||||
|
||||
- [ ] **Step 1: 정리 필터 확장**
|
||||
|
||||
`upsertUi()` 시작부의 필터를 CombatHud도 제거하도록 교체:
|
||||
|
||||
```js
|
||||
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud'));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: DeckHud `hud` push 직후, CombatHud 엔티티 생성 블록 추가**
|
||||
|
||||
`ui.ContentProto.Entities.push(...hud);` 직전에 아래 블록 삽입(헬퍼 `entity`/`transform`/`sprite`/`text`/`guid` 재사용):
|
||||
|
||||
```js
|
||||
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
||||
const combat = [];
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 0),
|
||||
path: '/ui/DefaultGroup/CombatHud',
|
||||
modelId: 'uiempty',
|
||||
entryId: 'UIEmpty',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
],
|
||||
}));
|
||||
// 적 패널 배경
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 1),
|
||||
path: '/ui/DefaultGroup/CombatHud/EnemyBg',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 0,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 170 }, pos: { x: 0, y: 300 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: PANEL_BG, type: 1 }),
|
||||
],
|
||||
}));
|
||||
const enemyTexts = [
|
||||
['EnemyName', { x: 0, y: 58 }, { x: 360, y: 44 }, '슬라임', 28, true, GOLD, 1],
|
||||
['EnemyHp', { x: 0, y: 16 }, { x: 360, y: 40 }, 'HP 45/45', 24, true, { r: 1, g: 1, b: 1, a: 1 }, 2],
|
||||
['EnemyBlock', { x: 0, y: -20 }, { x: 360, y: 36 }, '방어 0', 20, false, { r: 0.6, g: 0.8, b: 1, a: 1 }, 3],
|
||||
['EnemyIntent', { x: 0, y: -56 }, { x: 360, y: 38 }, '의도: 공격 10', 22, true, { r: 1, g: 0.72, b: 0.5, a: 1 }, 4],
|
||||
];
|
||||
let cmbN = 2;
|
||||
for (const [suffix, pos, size, value, fontSize, bold, color] of enemyTexts) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: enemyTexts.findIndex(([s]) => s === suffix) + 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize, bold, color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 플레이어 패널 배경 + 텍스트
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: '/ui/DefaultGroup/CombatHud/PlayerBg',
|
||||
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: 110 }, pos: { x: -760, y: -260 }, align: ALIGN_CENTER }),
|
||||
sprite({ color: PANEL_BG, type: 1 }),
|
||||
],
|
||||
}));
|
||||
const playerTexts = [
|
||||
['PlayerHp', { x: -760, y: -238 }, { x: 280, y: 44 }, 'HP 80/80', 26, true, { r: 1, g: 1, b: 1, a: 1 }],
|
||||
['PlayerBlock', { x: -760, y: -284 }, { x: 280, y: 38 }, '방어 0', 22, false, { r: 0.6, g: 0.8, b: 1, a: 1 }],
|
||||
];
|
||||
for (const [suffix, pos, size, value, fontSize, bold, color] of playerTexts) {
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 6 + playerTexts.findIndex(([s]) => s === suffix),
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize, bold, color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 결과 텍스트 (기본 숨김)
|
||||
const result = entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: '/ui/DefaultGroup/CombatHud/Result',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 8,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
result.jsonString.enable = false;
|
||||
combat.push(result);
|
||||
ui.ContentProto.Entities.push(...combat);
|
||||
```
|
||||
|
||||
`guid` 프리픽스 `'cmb'`를 위해 `guid()`의 ns 매핑에 분기 추가:
|
||||
|
||||
```js
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : 0xfe;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문법 검사**
|
||||
|
||||
Run: `node --check tools/gen-slaydeck.mjs`
|
||||
Expected: 오류 없음
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add tools/gen-slaydeck.mjs
|
||||
git commit -m "gen-slaydeck(B): CombatHud(적/플레이어 패널·결과) UI 엔티티 생성"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 재생성 + 검증
|
||||
|
||||
**Files:** 생성물 3종 (생성기 실행 결과)
|
||||
|
||||
- [ ] **Step 1: 생성기 실행**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs`
|
||||
Expected: `Slay deck UI and combat codeblocks generated.`
|
||||
|
||||
- [ ] **Step 2: 생성물 JSON 유효성 확인**
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8')); JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); console.log('JSON OK')"`
|
||||
Expected: `JSON OK`
|
||||
|
||||
- [ ] **Step 3: 결정성 확인 (2회 실행 동일)**
|
||||
|
||||
Run: `node tools/gen-slaydeck.mjs >/dev/null && sha1sum ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock > /tmp/a.sha && node tools/gen-slaydeck.mjs >/dev/null && sha1sum ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock > /tmp/b.sha && diff /tmp/a.sha /tmp/b.sha && echo DETERMINISTIC`
|
||||
Expected: `DETERMINISTIC`
|
||||
|
||||
- [ ] **Step 4: CombatHud 엔티티·전투 메서드 생성 확인**
|
||||
|
||||
Run: `grep -c "CombatHud" ui/DefaultGroup.ui; grep -c "DealDamageToEnemy\|EnemyTurn\|RenderCombat" RootDesk/MyDesk/SlayDeckController.codeblock`
|
||||
Expected: 두 값 모두 > 0
|
||||
|
||||
- [ ] **Step 5: 의도한 파일만 변경됐는지 확인**
|
||||
|
||||
Run: `git status --short`
|
||||
Expected: `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (그리고 필요 시 `Global/common.gamelogic`)만 변경.
|
||||
|
||||
- [ ] **Step 6: 생성물 커밋**
|
||||
|
||||
```bash
|
||||
git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock Global/common.gamelogic
|
||||
git commit -m "재생성(B): 카드 전투 통합 — 적/플레이어 전투 상태·CombatHud 반영"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 메이커 Play 수동 검증 (사용자)**
|
||||
|
||||
메이커에서 로컬 워크스페이스 reload 후 Play:
|
||||
- 타격 카드 클릭 → 적 HP 감소(적 Block 있으면 먼저 차감).
|
||||
- 방어 카드 클릭 → 플레이어 `방어` 수치 증가.
|
||||
- 턴 종료 → 적이 표시된 의도대로 공격(플레이어 Block이 피해 흡수) 또는 방어, 다음 의도 갱신.
|
||||
- 적 HP 0 → "승리!" 표시·입력 잠금 / 플레이어 HP 0 → "패배..." 표시·입력 잠금.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** 전투 상태(Task 2), 카드 수치화(Task 1), 효과 분기(Task 4), 적 의도·적 턴(Task 3·4), 승패(Task 3·4), UI 노출(Task 5) — 스펙 5개 절 모두 태스크로 매핑됨. 검증은 Task 6.
|
||||
- **Placeholder scan:** 모든 코드 단계에 실제 코드 포함. "TODO(E)"는 의도된 미래 훅 주석(스펙 명시)으로 placeholder 아님.
|
||||
- **Type consistency:** UI 경로(`/ui/DefaultGroup/CombatHud/EnemyHp` 등)가 codeblock `RenderCombat`/`ShowResult`와 Task 5 생성 경로에서 동일. 메서드명(`DealDamageToEnemy`/`DealDamageToPlayer`/`EnemyTurn`/`CheckCombatEnd`/`ShowResult`/`RenderCombat`)이 호출부(Task 4)와 정의부(Task 3)에서 일치. 카드 필드(`damage`/`block`/`kind`)가 Cards 정의(Task 1)와 PlayCard 사용(Task 4)에서 일치.
|
||||
Reference in New Issue
Block a user