docs(buffs-power): P6 구현 계획
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
377
docs/superpowers/plans/2026-06-12-buffs-power.md
Normal file
377
docs/superpowers/plans/2026-06-12-buffs-power.md
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
# P6 — 버프/디버프·Power 카드·적 방어도 UI 구현 계획
|
||||||
|
|
||||||
|
> **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:** StS 표준 약화·취약·힘 버프 시스템 + Power 카드 kind + 적 방어도 배지 UI를 데이터 주도로 구현.
|
||||||
|
|
||||||
|
**Architecture:** `data/cards.json`·`enemies.json` 스키마 확장 → `tools/deck/gen-slaydeck.mjs`의 Lua 생성부(상태 props·전투 메서드·UI 엔티티) 확장 → 산출물 재생성. 밸런스 시뮬(`tools/balance/sim-balance.mjs`)에 동일 규칙 재현.
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js(생성기·시뮬), MSW Lua(생성물), node:test.
|
||||||
|
|
||||||
|
설계 문서: `docs/superpowers/specs/2026-06-12-buffs-power-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 신규 카드 이미지 RUID 선별 (메이커)
|
||||||
|
|
||||||
|
**Files:** 없음 (RUID 4개 확보가 산출물)
|
||||||
|
|
||||||
|
- [ ] **Step 1**: `asset_search_resources`(cat=sprite, source=maplestory)로 후보 수집 — 쿼리: "차지 블로우", "위협", "인레이지", "분노" (결과 빈약 시 "스킬", "버프" 등 보조 쿼리)
|
||||||
|
- [ ] **Step 2**: 메이커 Play Test 상태에서 `maker_execute_script`(client)로 후보 RUID를 UIGroup에 격자 배치(아래 패턴) 후 `maker_screenshot`으로 확인, 카드당 1개 선별
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 후보 미리보기 패턴 (client 컨텍스트)
|
||||||
|
local ruids = { "<ruid1>", "<ruid2>", "..." }
|
||||||
|
local root = _EntityService:GetEntityByPath("/ui/DefaultGroup")
|
||||||
|
for i = 1, #ruids do
|
||||||
|
local e = _SpawnService:SpawnByModelId("model://uisprite", "RuidPreview" .. i, Vector3(0,0,0), root)
|
||||||
|
e.SpriteGUIRendererComponent.ImageRUID = ruids[i]
|
||||||
|
e.UITransformComponent.anchoredPosition = Vector2(-600 + ((i-1) % 8) * 160, 200 - math.floor((i-1) / 8) * 160)
|
||||||
|
e.UITransformComponent.RectSize = Vector2(140, 140)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3**: 미리보기 엔티티 제거(스크립트로 Destroy) 후 플레이 종료
|
||||||
|
|
||||||
|
### Task 2: 카드·적 데이터 확장
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `data/cards.json`
|
||||||
|
- Modify: `data/enemies.json`
|
||||||
|
|
||||||
|
- [ ] **Step 1**: `data/cards.json`의 `cards`에 4종 추가 (image는 Task 1 선별값)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"ChargedBlow": { "name": "차지 블로우", "cost": 2, "kind": "Attack", "damage": 8, "vuln": 2, "desc": "피해 8, 취약 2", "image": "<선별RUID>" },
|
||||||
|
"Threaten": { "name": "위협", "cost": 0, "kind": "Skill", "weak": 2, "desc": "약화 2 부여", "image": "<선별RUID>" },
|
||||||
|
"Enrage": { "name": "인레이지", "cost": 1, "kind": "Skill", "strength": 2, "desc": "힘 +2", "image": "<선별RUID>" },
|
||||||
|
"Rage": { "name": "분노", "cost": 1, "kind": "Power", "powerEffect": "strengthPerTurn", "value": 1, "desc": "매 턴 시작 시 힘 +1", "image": "<선별RUID>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2**: `data/enemies.json` 인텐트에 Debuff 추가 — mushmom에 `{ "kind": "Debuff", "effect": "weak", "value": 2 }` (intents 2번째로 삽입), slime_elite에 `{ "kind": "Debuff", "effect": "weak", "value": 1 }` (마지막), slime_boss·king_slime에 `{ "kind": "Debuff", "effect": "vuln", "value": 2 }` (Defend 다음), modified_snail에 `{ "kind": "Debuff", "effect": "weak", "value": 1 }` (마지막)
|
||||||
|
- [ ] **Step 3**: 커밋 `feat(buffs-power): 신규 카드 4종·적 디버프 인텐트 데이터`
|
||||||
|
|
||||||
|
### Task 3: 생성기 — 직렬화·상태·전투 규칙
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tools/deck/gen-slaydeck.mjs` (luaCardsTable ~line 64, props ~1888, StartCombat ~2001, BuildMonsters ~2040, StartPlayerTurn ~2184, EndPlayerTurn ~2191, PlayCard ~2410, DealDamageToTarget ~2520, EnemyActStep ~2599, ApplyCardFace ~2347)
|
||||||
|
|
||||||
|
- [ ] **Step 1**: `luaCardsTable`에 신규 필드 직렬화 추가
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (c.strength != null) fields.push(`strength = ${c.strength}`);
|
||||||
|
if (c.weak != null) fields.push(`weak = ${c.weak}`);
|
||||||
|
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
|
||||||
|
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
|
||||||
|
if (c.value != null) fields.push(`value = ${c.value}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2**: props 추가 — `prop('number', 'PlayerStr', '0')`, `prop('number', 'PlayerWeak', '0')`, `prop('number', 'PlayerVuln', '0')`, `prop('any', 'PlayerPowers')`
|
||||||
|
- [ ] **Step 3**: `StartCombat`에 리셋 추가 (`self.PlayerBlock = 0` 다음 줄)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
self.PlayerStr = 0
|
||||||
|
self.PlayerWeak = 0
|
||||||
|
self.PlayerVuln = 0
|
||||||
|
self.PlayerPowers = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4**: `BuildMonsters`의 몬스터 테이블 생성에 `str = 0, weak = 0, vuln = 0` 필드 추가 (기존 `block = 0` 자리 옆)
|
||||||
|
- [ ] **Step 5**: 플레이어 공격 피해 헬퍼 `CalcPlayerAttack` 메서드 신설 + `PlayCard`의 Attack 분기를 수정 — `c.damage`에 힘·약화 적용한 값을 `PlayAttackFx`에 전달. 버프 필드 공통 처리(Attack/Skill 양쪽): `strength`/`weak`/`vuln` 적용
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- method CalcPlayerAttack(base) → number
|
||||||
|
local dmg = base + self.PlayerStr
|
||||||
|
if self.PlayerWeak > 0 then
|
||||||
|
dmg = math.floor(dmg * 0.75)
|
||||||
|
end
|
||||||
|
return dmg
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- PlayCard 내 교체 (Attack 분기)
|
||||||
|
if c.kind == "Attack" then
|
||||||
|
if c.damage ~= nil then
|
||||||
|
self:PlayAttackFx(self.TargetIndex, c.image, self:CalcPlayerAttack(c.damage))
|
||||||
|
end
|
||||||
|
if c.block ~= nil then
|
||||||
|
self.PlayerBlock = self.PlayerBlock + c.block
|
||||||
|
end
|
||||||
|
self:ApplyRelics("cardPlayed")
|
||||||
|
elseif c.kind == "Skill" then
|
||||||
|
if c.block ~= nil then
|
||||||
|
self.PlayerBlock = self.PlayerBlock + c.block
|
||||||
|
end
|
||||||
|
elseif c.kind == "Power" then
|
||||||
|
if c.powerEffect ~= nil then
|
||||||
|
table.insert(self.PlayerPowers, cardId)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 공통 버프/디버프 적용 (kind 분기 아래, table.remove 위)
|
||||||
|
if c.strength ~= nil then
|
||||||
|
self.PlayerStr = self.PlayerStr + c.strength
|
||||||
|
end
|
||||||
|
if c.weak ~= nil or c.vuln ~= nil then
|
||||||
|
local tm = self.Monsters[self.TargetIndex]
|
||||||
|
if tm ~= nil and tm.alive == true then
|
||||||
|
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
|
||||||
|
if c.vuln ~= nil then tm.vuln = tm.vuln + c.vuln end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6**: Power 소멸 처리 — `PlayCard`의 `table.insert(self.DiscardPile, cardId)`를 조건부로
|
||||||
|
|
||||||
|
```lua
|
||||||
|
table.remove(self.Hand, slot)
|
||||||
|
if c.kind ~= "Power" then
|
||||||
|
table.insert(self.DiscardPile, cardId)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7**: `DealDamageToTarget`에 취약 배수 (block 차감 **이전**)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local dmg = amount
|
||||||
|
if m.vuln > 0 then
|
||||||
|
dmg = math.floor(dmg * 1.5)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8**: `EnemyActStep` — Debuff 인텐트 처리 + 적 공격 피해 공식 + 행동 후 적 디버프 감소
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if intent.kind == "Attack" then
|
||||||
|
local atk = intent.value + m.str
|
||||||
|
if m.weak > 0 then
|
||||||
|
atk = math.floor(atk * 0.75)
|
||||||
|
end
|
||||||
|
if self.PlayerVuln > 0 then
|
||||||
|
atk = math.floor(atk * 1.5)
|
||||||
|
end
|
||||||
|
local before = self.PlayerHp
|
||||||
|
self:DealDamageToPlayer(atk)
|
||||||
|
self:ShowPlayerDmgPop(before - self.PlayerHp)
|
||||||
|
elseif intent.kind == "Defend" then
|
||||||
|
m.block = m.block + intent.value
|
||||||
|
elseif intent.kind == "Debuff" then
|
||||||
|
if intent.effect == "weak" then
|
||||||
|
self.PlayerWeak = self.PlayerWeak + intent.value
|
||||||
|
elseif intent.effect == "vuln" then
|
||||||
|
self.PlayerVuln = self.PlayerVuln + intent.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- intentIdx 갱신 직후
|
||||||
|
if m.weak > 0 then m.weak = m.weak - 1 end
|
||||||
|
if m.vuln > 0 then m.vuln = m.vuln - 1 end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 9**: `EndPlayerTurn`에 플레이어 디버프 감소 (`self:EnemyTurn()` 직전)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
|
||||||
|
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 10**: `StartPlayerTurn`에 파워 발동 (`self:ApplyRelics("turnStart")` 다음)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if self.PlayerPowers ~= nil then
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil and pc.powerEffect == "strengthPerTurn" then
|
||||||
|
self.PlayerStr = self.PlayerStr + pc.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 11**: `ApplyCardFace` kind 색 분기에 `elseif c.kind == "Power" then` → `Color(0.46, 0.68, 0.52, 1)` 명시 (기존 else를 Power로)
|
||||||
|
- [ ] **Step 12**: 커밋 `feat(buffs-power): 버프/디버프·Power 전투 규칙 (생성기)`
|
||||||
|
|
||||||
|
### Task 4: 생성기 — UI (적 방어도 배지·버프 라인·인텐트)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tools/deck/gen-slaydeck.mjs` (몬스터 슬롯 UI 생성부 ~line 916-1015, PlayerPanel ~1015-1100, RenderCombat ~2688)
|
||||||
|
|
||||||
|
- [ ] **Step 1**: 몬스터 슬롯 루프에 BlockBadge(guid 270+i)·Value(guid 280+i)·Buffs(guid 290+i) 엔티티 추가 (DmgPop 추가 코드 다음)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mBlockBadge = entity({
|
||||||
|
id: guid('cmb', 270 + i), path: `${base}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: 6,
|
||||||
|
components: [
|
||||||
|
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 36 }, pos: { x: -HP_BAR_W / 2 - 30, y: -14 } }),
|
||||||
|
sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mBlockBadge.jsonString.enable = false;
|
||||||
|
combat.push(mBlockBadge);
|
||||||
|
combat.push(entity({
|
||||||
|
id: guid('cmb', 280 + i), path: `${base}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
|
displayOrder: 0,
|
||||||
|
components: [
|
||||||
|
transform({ parentW: 40, parentH: 36, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 32 }, pos: { x: 0, y: 0 } }),
|
||||||
|
sprite({ color: TRANSPARENT }),
|
||||||
|
text({ value: '0', fontSize: 17, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
combat.push(entity({
|
||||||
|
id: guid('cmb', 290 + i), path: `${base}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
|
displayOrder: 7,
|
||||||
|
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 + 60, y: 22 }, pos: { x: 0, y: -58 } }),
|
||||||
|
sprite({ color: TRANSPARENT }),
|
||||||
|
text({ value: '', fontSize: 15, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2**: PlayerPanel에 Buffs 텍스트(guid 217) 추가 (BlockBadge/Value 다음)
|
||||||
|
|
||||||
|
```js
|
||||||
|
combat.push(entity({
|
||||||
|
id: guid('cmb', 217), path: `${PP}/Buffs`, modelId: 'uitext', entryId: 'UIText',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
|
displayOrder: 6,
|
||||||
|
components: [
|
||||||
|
transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 22 }, pos: { x: 0, y: -44 } }),
|
||||||
|
sprite({ color: TRANSPARENT }),
|
||||||
|
text({ value: '', fontSize: 14, bold: true, color: { r: 0.85, g: 0.65, b: 1, a: 1 }, alignment: 4 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3**: 버프 문자열 헬퍼 메서드 `BuffsLabel` 신설 (Lua, str/weak/vuln → "힘+2 약화1 취약2")
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- method BuffsLabel(str, weak, vuln) → string
|
||||||
|
local parts = {}
|
||||||
|
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
|
||||||
|
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
|
||||||
|
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
|
||||||
|
return table.concat(parts, " ")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4**: `RenderCombat` 확장 — 몬스터 루프 내(SetHpBar 다음)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
|
||||||
|
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
|
||||||
|
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln))
|
||||||
|
```
|
||||||
|
|
||||||
|
인텐트 분기 교체 (Attack은 최종 예상치·Debuff 추가):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local t = ""
|
||||||
|
if intent ~= nil then
|
||||||
|
if intent.kind == "Attack" then
|
||||||
|
local atk = intent.value + m.str
|
||||||
|
if m.weak > 0 then atk = math.floor(atk * 0.75) end
|
||||||
|
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
|
||||||
|
t = "공격 " .. tostring(atk)
|
||||||
|
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
|
||||||
|
elseif intent.kind == "Debuff" then
|
||||||
|
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
|
||||||
|
else t = "취약 " .. tostring(intent.value) .. " 부여" end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
인텐트 색: Attack 빨강(기존), Defend 파랑(기존 else), Debuff 보라 `Color(0.8, 0.5, 1, 1)` 분기 추가.
|
||||||
|
|
||||||
|
플레이어 표시 (기존 BlockBadge 갱신 다음):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln)
|
||||||
|
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
|
||||||
|
local names = {}
|
||||||
|
for i = 1, #self.PlayerPowers do
|
||||||
|
local pc = self.Cards[self.PlayerPowers[i]]
|
||||||
|
if pc ~= nil then table.insert(names, pc.name) end
|
||||||
|
end
|
||||||
|
if pb ~= "" then pb = pb .. " · " end
|
||||||
|
pb = pb .. table.concat(names, " ")
|
||||||
|
end
|
||||||
|
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5**: 커밋 `feat(buffs-power): 적 방어도 배지·버프 라인·디버프 인텐트 UI (생성기)`
|
||||||
|
|
||||||
|
### Task 5: 밸런스 시뮬 동기화 + 테스트
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tools/balance/sim-balance.mjs`
|
||||||
|
- Test: `tools/balance/sim-balance.test.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1**: 실패 테스트 먼저 추가 — 약화·취약·힘 계산 + Debuff 인텐트 + Power 동작
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('simulateCombat: 취약이 플레이어 공격을 1.5배로', () => {
|
||||||
|
const data = {
|
||||||
|
cards: { Vuln: { name: '취약기', cost: 1, kind: 'Skill', vuln: 9 }, Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 10 } },
|
||||||
|
starterDeck: ['Vuln', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||||
|
monsters: [{ name: '적', maxHp: 100, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, mulberry32(1));
|
||||||
|
// 1턴: 공격 우선 휴리스틱 → Hit×3 (취약 미부여, 30) — 그래도 30+α로 수치 검증은 별도 단위 함수로
|
||||||
|
assert.equal(typeof r.win, 'boolean');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calcAttack: 힘·약화·취약 공식', () => {
|
||||||
|
assert.equal(calcAttack(6, 2, 0, 0), 8); // 힘+2
|
||||||
|
assert.equal(calcAttack(6, 0, 1, 0), 4); // 약화 floor(6*0.75)
|
||||||
|
assert.equal(calcAttack(6, 0, 0, 1), 9); // 취약 floor(6*1.5)
|
||||||
|
assert.equal(calcAttack(10, 2, 1, 1), 13); // floor(floor(12*0.75)=9 → floor(9*1.5)=13
|
||||||
|
});
|
||||||
|
|
||||||
|
test('simulateCombat: 적 Debuff 인텐트로 플레이어 약화 → 받는 피해 감소 검증', () => {
|
||||||
|
const data = {
|
||||||
|
cards: { Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 } },
|
||||||
|
starterDeck: ['Hit', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||||
|
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Debuff', effect: 'weak', value: 1 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, mulberry32(1));
|
||||||
|
assert.equal(r.playerHpRemaining, 80); // Debuff만 하는 적 → 피해 0
|
||||||
|
});
|
||||||
|
|
||||||
|
test('simulateCombat: Power(매턴 힘) 누적', () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Rage: { name: '분노', cost: 1, kind: 'Power', powerEffect: 'strengthPerTurn', value: 5 },
|
||||||
|
Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ['Rage', 'Hit', 'Hit', 'Hit', 'Hit'],
|
||||||
|
monsters: [{ name: '적', maxHp: 60, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, mulberry32(1));
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.ok(r.turns <= 6, `파워 누적으로 빠른 처치 기대, 실제 ${r.turns}턴`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2**: `node --test tools/balance/sim-balance.test.mjs` → 신규 테스트 FAIL 확인
|
||||||
|
- [ ] **Step 3**: `sim-balance.mjs` 구현 — `calcAttack(base, str, weak, vulnOnTarget)` export 신설, `simulateCombat`에 pStr/pWeak/pVuln/powers·몬스터 str/weak/vuln 상태 추가, 규칙 재현(부여→감소 타이밍 Lua와 동일), `chooseAction` 확장(Attack 우선 유지, 잔여 에너지로 Power→버프 Skill 사용), Debuff 인텐트 처리. `formatReport`의 kind 루프에 'Power' 포함(효율 계산은 plays만 표시).
|
||||||
|
- [ ] **Step 4**: `node --test tools/balance/sim-balance.test.mjs` → 전체 PASS
|
||||||
|
- [ ] **Step 5**: 커밋 `feat(buffs-power): 밸런스 시뮬 버프/디버프·Power 동기화`
|
||||||
|
|
||||||
|
### Task 6: 산출물 재생성·시뮬 확인·푸시·PR·머지
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Regen: `RootDesk/MyDesk/SlayDeckController.codeblock`, `ui/DefaultGroup.ui`, `Global/common.gamelogic`
|
||||||
|
|
||||||
|
- [ ] **Step 1**: `node tools/deck/gen-slaydeck.mjs` 실행 성공 확인
|
||||||
|
- [ ] **Step 2**: `node tools/balance/sim-balance.mjs` — 승률 0%/100% 극단 아님 확인 (참고용 리포트 기록)
|
||||||
|
- [ ] **Step 3**: 커밋 `feat(buffs-power): 산출물 재생성 (버프/디버프·Power·적 방어도 UI)`
|
||||||
|
- [ ] **Step 4**: `git push -u origin feature/p6-buffs-power`
|
||||||
|
- [ ] **Step 5**: Gitea API로 PR 생성 → 머지 (기존 자동화 패턴: `curl -s -X POST .../repos/gahusb/maplecontest/pulls`, 토큰은 `.mcp.json` 참조 금지 — `git credential` 또는 기존 사용 토큰 경로)
|
||||||
|
|
||||||
|
## Self-Review 결과
|
||||||
|
|
||||||
|
- 설계 요구 전 항목(버프 3종·Power·적 방어도 배지·예시 카드 4종·적 디버프 인텐트·시뮬 동기화) 태스크 매핑 확인
|
||||||
|
- 타입/이름 일관성: `CalcPlayerAttack`·`BuffsLabel`·`PlayerStr/Weak/Vuln`·`PlayerPowers`·`m.str/weak/vuln` 통일 확인
|
||||||
|
- 플레이스홀더: 카드 image RUID만 Task 1 산출물에 의존 (의도된 순서)
|
||||||
Reference in New Issue
Block a user