From 248677759c3b26275d224471b8ceeabda1ffeaa6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 08:55:44 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs(system-gaps):=20P5=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=C2=B7=EA=B3=84=ED=9A=8D=20(=EA=B2=BD=EC=A0=9C=C2=B7?= =?UTF-8?q?=EB=B3=B5=ED=95=A9=EC=B9=B4=EB=93=9C=C2=B7=EC=A0=81=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=C2=B7=EB=9F=B0=EC=A2=85=EB=A3=8C=20=EB=B3=B5=EA=B7=80?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-11-system-gaps.md | 79 +++++++++++++++++++ .../specs/2026-06-11-system-gaps-design.md | 53 +++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-11-system-gaps.md create mode 100644 docs/superpowers/specs/2026-06-11-system-gaps-design.md diff --git a/docs/superpowers/plans/2026-06-11-system-gaps.md b/docs/superpowers/plans/2026-06-11-system-gaps.md new file mode 100644 index 0000000..655824d --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-system-gaps.md @@ -0,0 +1,79 @@ +# 시스템 갭 보완 (P5) 구현 계획 + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. T3·T5는 컨트롤러 직접. + +**Goal:** 경제 밸런스(첫 상점 구매 가능), 신규 카드 2종+복합 효과, 적 패턴 보강, 런 종료 후 메뉴 복귀. + +--- + +## Task 1: 데이터+상수 (경제·적 패턴·신규 카드 골격) + +**Files:** `data/enemies.json`, `data/cards.json`, `tools/deck/gen-slaydeck.mjs`(상수·elite 골드) + +- [ ] enemies.json 의도 패턴 교체(스펙 §2.C 표 그대로; slime 3종·king_slime 유지). +- [ ] cards.json에 추가(이미지는 T3에서 채움 — 일단 필드 생략): +```json + "WarLeap": { "name": "워 리프", "cost": 1, "kind": "Attack", "damage": 4, "block": 3, "desc": "피해 4, 방어도 3" }, + "Brandish": { "name": "브랜디시", "cost": 2, "kind": "Attack", "damage": 13, "desc": "피해 13" } +``` +- [ ] gen-slaydeck: `GOLD_PER_WIN = 15` → `25`; CheckCombatEnd elite 분기(AddRelic 줄 옆)에 `self.Gold = self.Gold + 15` 추가. +- [ ] 검증: JSON 파스, gen-slaydeck 실행 OK(산출물 복원), sim 통과(기존 fixture 무관). +- [ ] Commit: `feat(system-gaps): 경제 상향(승리25·엘리트+15)·적 패턴 보강·신규 카드 2종 데이터` + +## Task 2: 복합 카드 로직 + EndRun 복귀 + sim + +**Files:** `tools/deck/gen-slaydeck.mjs`, `tools/balance/sim-balance.mjs`, `tools/balance/sim-balance.test.mjs` + +- [ ] **sim 테스트 먼저** (test 파일에 추가): +```js +test('simulateCombat: Attack 카드의 block 필드도 적용(복합 카드)', () => { + const data = { + cards: { Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 4, block: 3 } }, + starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'], + monsters: [{ name: '적', maxHp: 9, intents: [{ kind: 'Attack', value: 5 }] }], + }; + const r = simulateCombat(data, mulberry32(1)); + assert.equal(r.win, true); // 3코스트 내 3장: 12딤>9 → 1턴 승리(블록 적용 여부와 무관하게 승리하지만) + assert.equal(r.playerHpRemaining, 80); // 피해 받기 전 승리 — 블록 검증은 아래 시나리오로 +}); +test('simulateCombat: 복합 카드 블록이 적 공격을 흡수', () => { + const data = { + cards: { Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 1, block: 3 } }, + starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'], + monsters: [{ name: '적', maxHp: 100, intents: [{ kind: 'Attack', value: 9 }] }], + }; + const r = simulateCombat(data, mulberry32(1)); + // 1턴: 3장 사용 → 블록 9 → 적 공격 9 전부 흡수 → 2턴 시작 HP 80 유지 확인 위해 2턴 후 비교 불가(루프) — 간접: MAX_TURNS 도달 draw, hp가 (80 - 0*몇턴)... + // 단순 명제: 블록 미적용이면 매턴 -9 → 100/9≈11턴 내 사망. 블록 적용이면 매턴 9블록=무피해 → draw. + assert.equal(r.draw, true); + assert.equal(r.playerHpRemaining, 80); +}); +``` +- [ ] sim 구현: simulateCombat Attack 분기에 `if (c.block) pBlock += c.block;` 추가(스탯 bump의 block 합산도 `c.block || 0`로). 테스트 통과. +- [ ] gen-slaydeck PlayCard Attack 분기에 추가(PlayAttackFx 호출 다음 줄): +``` + if c.block ~= nil then + self.PlayerBlock = self.PlayerBlock + c.block + end +``` +- [ ] gen-slaydeck EndRun: 신규 메서드 + CheckCombatEnd 두 지점 교체: +```js + method('EndRun', `self:ShowResult(text) +self.RunActive = false +_TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), +``` +교체: `self:ShowResult("런 클리어!")` + `self.RunActive = false` → `self:EndRun("런 클리어!")`; `self:ShowResult("패배...")` + `self.RunActive = false` → `self:EndRun("패배...")`. +- [ ] 검증: sim 전체 통과(16개), gen 실행·심볼 확인 후 산출물 복원. +- [ ] Commit: `feat(system-gaps): 복합 카드(피해+방어)·런 종료 후 메뉴 복귀(EndRun)` + +## Task 3 (컨트롤러 직접): 신규 카드 이미지 수확 +- "워 리프"/"브랜디시" 검색→메이커 선별→cards.json image 채움→커밋. + +## Task 4: 재생성·검증·산출물 커밋 (T3 이후) +- 표준 절차 + `node tools/balance/sim-balance.mjs 2000` 결과 기록(참고). + +## Task 5 (컨트롤러 직접): 메이커 검증+푸시+PR+머지 +- 보상/상점 신규 카드(이미지)·복합 카드 효과·엘리트 골드·패배→4s 메뉴 복귀. 스크린샷. + +## Self-Review +- §2.A→T1, §2.B→T1(데이터)+T2(로직)+T3(이미지), §2.C→T1, §2.D→T2. 시그니처 일관(EndRun(text)). 복합 카드 sim 테스트는 블록 적용을 draw/hp로 결정적으로 판별. diff --git a/docs/superpowers/specs/2026-06-11-system-gaps-design.md b/docs/superpowers/specs/2026-06-11-system-gaps-design.md new file mode 100644 index 0000000..069c9e9 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-system-gaps-design.md @@ -0,0 +1,53 @@ +# 시스템 갭 보완 (P5) — 설계 + +- 날짜: 2026-06-11 +- 상태: 승인됨(사용자 사전 위임). 로드맵 P5/5 (선별 범위). + +## 1. 선별 항목 (임팩트/리스크 기준) + +| # | 항목 | 문제 | 해결 | +|---|---|---|---| +| A | 경제 밸런스 | 골드/승리 15 < 카드 30 → 첫 상점 구매 불가 | `GOLD_PER_WIN` 15→25, 엘리트 승리 보너스 골드 +15(유물에 더해) | +| B | 카드 풀 | 3종뿐 — 보상/상점 단조 | 신규 2종 + **복합 효과(damage+block 동시) 지원** | +| C | 적 패턴 | 신규 몬스터 의도 2~3스텝 단조 | enemies.json 패턴 보강(3~4스텝, 강공 텔레그래프) | +| D | 런 루프 미완결 | 클리어/패배 후 Result 텍스트만(메뉴 복귀 없음) | 결과 표시 4s 후 `ShowMainMenu` 자동 복귀 | + +범위 제외: 저장(E6b 사용자 보류), 카드 제거/업그레이드/포션/이벤트 노드(후속). + +## 2. 설계 + +### 2.A 경제 +- `GOLD_PER_WIN = 25` (gen-slaydeck 상수). +- `CheckCombatEnd` elite 분기에 `self.Gold = self.Gold + 15` 추가(유물 지급 유지). + +### 2.B 복합 카드 +- 규칙 확장: **Attack 카드에 block 필드가 있으면 방어도 함께 적용** (Skill은 기존대로 block만). + - Lua `PlayCard`: Attack 분기에서 `if c.block ~= nil then self.PlayerBlock = self.PlayerBlock + c.block end` 추가(데미지는 기존 PlayAttackFx 경로). + - sim `simulateCombat`: Attack 분기에 동일 추가 + 테스트 1종. +- 신규 카드(`data/cards.json`): + - `WarLeap`(워 리프): cost 1, Attack, damage 4, block 3, desc "피해 4, 방어도 3" — 복합. + - `Brandish`(브랜디시): cost 2, Attack, damage 13, desc "피해 13" — 고코스트 딜. + - 이미지: 공식 RUID 수확(검증된 워크플로 — "워 리프"/"브랜디시" 질의, 부적합 시 기존 보조 질의). +- 시작 덱 불변(신규 카드는 보상/상점 풀에서 등장 — 풀은 `self.Cards` 전체이므로 자동 포함). + +### 2.C 적 패턴 (enemies.json) +- orange_mushroom: [공5, 방4, 공7] → [공5, 공5, 방4, 공8] (빌드업) +- green_mushroom: [공7, 공4] → [공7, 방3, 공9] +- pig: [공6, 방3] → [공6, 공6, 방5] +- blue_mushroom: [공8, 공4] → [공4, 공4, 공10] (텔레그래프형) +- mushmom: [공14, 방10, 공9] → [방10, 공16, 공9, 방6] +- modified_snail: [공12, 공7, 방8] → [공12, 방8, 공7, 공14] +- king_slime: 유지(이미 4스텝). slime/slime_elite/slime_boss: 유지(레거시 호환). + +### 2.D 런 종료 복귀 +- `ShowResult(text)` 호출 후 `RunActive=false`가 되는 두 곳(런 클리어·패배)에서: 4초 타이머 → `self:ShowMainMenu()`. +- 구현: `ShowResult`에 두 번째 동작 추가 대신 **새 메서드 `EndRun(text)`** = ShowResult(text) + RunActive=false + 4s 타이머 ShowMainMenu. CheckCombatEnd의 두 지점("런 클리어!"/"패배...")을 EndRun 호출로 교체. +- ShowMainMenu는 ShowState("menu")로 전 HUD 정리(기존) — Result는 CombatHud 자식이라 같이 숨겨짐. 단 Result.Enable 자체는 StartCombat에서 리셋(기존). + +## 3. 검증 +- sim: 복합 카드 테스트 추가, 전체 통과. `node tools/balance/sim-balance.mjs 2000`으로 새 패턴 승률 출력(참고 기록 — 100%면 여전히 약함이나 P5 범위는 구조, 수치 정밀 튜닝은 후속). +- 메이커: 보상/상점에서 신규 카드 등장(이미지 포함), 복합 카드 사용 시 피해+방어 동시, 패배 또는 클리어 → 4s 후 메뉴 복귀, 엘리트 승리 골드 +15+25. + +## 4. 리스크 +- 복합 카드의 sim AI(chooseAction)는 Attack 우선 로직 그대로(블록 가치 미평가) — 밸런스 추정 약간 보수적, 허용. +- EndRun 타이머 중 사용자가 이미 메뉴로 못 가는 상태(입력 잠금) — CombatOver=true가 가드. From 1de8fac893f69473cec103fe5be604a52b50dc27 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 08:57:33 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(system-gaps):=20=EA=B2=BD=EC=A0=9C=20?= =?UTF-8?q?=EC=83=81=ED=96=A5(=EC=8A=B9=EB=A6=AC25=C2=B7=EC=97=98=EB=A6=AC?= =?UTF-8?q?=ED=8A=B8+15)=C2=B7=EC=A0=81=20=ED=8C=A8=ED=84=B4=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95=C2=B7=EC=8B=A0=EA=B7=9C=20=EC=B9=B4=EB=93=9C=202?= =?UTF-8?q?=EC=A2=85=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- data/cards.json | 4 +++- data/enemies.json | 22 ++++++++++++++-------- tools/deck/gen-slaydeck.mjs | 3 ++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/data/cards.json b/data/cards.json index 2cf4171..8946935 100644 --- a/data/cards.json +++ b/data/cards.json @@ -2,7 +2,9 @@ "cards": { "Strike": { "name": "파워 스트라이크", "cost": 1, "kind": "Attack", "damage": 6, "desc": "피해 6", "image": "a71b116807904ef2b38e1dc013e2f9a2" }, "Defend": { "name": "아이언 바디", "cost": 1, "kind": "Skill", "block": 5, "desc": "방어도 5", "image": "1ae9b6741c5947a8b528a0f515b50e3e" }, - "Bash": { "name": "슬래시 블러스트", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10", "image": "d5bc2953fcab4cfe9062af81c35aff86" } + "Bash": { "name": "슬래시 블러스트", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10", "image": "d5bc2953fcab4cfe9062af81c35aff86" }, + "WarLeap": { "name": "워 리프", "cost": 1, "kind": "Attack", "damage": 4, "block": 3, "desc": "피해 4, 방어도 3" }, + "Brandish": { "name": "브랜디시", "cost": 2, "kind": "Attack", "damage": 13, "desc": "피해 13" } }, "starterDeck": ["Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash"] } diff --git a/data/enemies.json b/data/enemies.json index ab47c11..c051043 100644 --- a/data/enemies.json +++ b/data/enemies.json @@ -32,17 +32,19 @@ "name": "주황버섯", "maxHp": 16, "intents": [ + { "kind": "Attack", "value": 5 }, { "kind": "Attack", "value": 5 }, { "kind": "Defend", "value": 4 }, - { "kind": "Attack", "value": 7 } + { "kind": "Attack", "value": 8 } ] }, "blue_mushroom": { "name": "파란버섯", "maxHp": 22, "intents": [ - { "kind": "Attack", "value": 8 }, - { "kind": "Attack", "value": 4 } + { "kind": "Attack", "value": 4 }, + { "kind": "Attack", "value": 4 }, + { "kind": "Attack", "value": 10 } ] }, "pig": { @@ -50,7 +52,8 @@ "maxHp": 18, "intents": [ { "kind": "Attack", "value": 6 }, - { "kind": "Defend", "value": 3 } + { "kind": "Attack", "value": 6 }, + { "kind": "Defend", "value": 5 } ] }, "green_mushroom": { @@ -58,16 +61,18 @@ "maxHp": 20, "intents": [ { "kind": "Attack", "value": 7 }, - { "kind": "Attack", "value": 4 } + { "kind": "Defend", "value": 3 }, + { "kind": "Attack", "value": 9 } ] }, "mushmom": { "name": "머쉬맘", "maxHp": 75, "intents": [ - { "kind": "Attack", "value": 14 }, { "kind": "Defend", "value": 10 }, - { "kind": "Attack", "value": 9 } + { "kind": "Attack", "value": 16 }, + { "kind": "Attack", "value": 9 }, + { "kind": "Defend", "value": 6 } ] }, "modified_snail": { @@ -75,8 +80,9 @@ "maxHp": 60, "intents": [ { "kind": "Attack", "value": 12 }, + { "kind": "Defend", "value": 8 }, { "kind": "Attack", "value": 7 }, - { "kind": "Defend", "value": 8 } + { "kind": "Attack", "value": 14 } ] }, "king_slime": { diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 5855687..1c88cea 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -1833,7 +1833,7 @@ function codeblock(id, name, properties, methods) { function writeCodeblocks() { const RUN_LENGTH = 3; - const GOLD_PER_WIN = 15; + const GOLD_PER_WIN = 25; const CARD_PRICE = 30; const REST_HEAL = 30; const RELIC_PRICE = 60; @@ -2641,6 +2641,7 @@ if anyAlive == false then self:RenderRun() local node = self.MapNodes[self.CurrentNodeId] if node ~= nil and node.type == "elite" then + self.Gold = self.Gold + 15 self:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)]) end if node ~= nil and node.type == "boss" then From 8ff50b428da32f9b203a9e52cbbd940ae6b6c11c Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 09:01:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat(system-gaps):=20=EB=B3=B5=ED=95=A9=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C(=ED=94=BC=ED=95=B4+=EB=B0=A9=EC=96=B4)=C2=B7?= =?UTF-8?q?=EB=9F=B0=20=EC=A2=85=EB=A3=8C=20=ED=9B=84=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=20=EB=B3=B5=EA=B7=80(EndRun)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tools/balance/sim-balance.mjs | 3 ++- tools/balance/sim-balance.test.mjs | 13 +++++++++++++ tools/deck/gen-slaydeck.mjs | 12 ++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tools/balance/sim-balance.mjs b/tools/balance/sim-balance.mjs index b5a244f..209e970 100644 --- a/tools/balance/sim-balance.mjs +++ b/tools/balance/sim-balance.mjs @@ -120,7 +120,8 @@ export function simulateCombat(data, rng, stats) { const r = applyDamage(target.hp, target.block, c.damage || 0); target.hp = r.hp; target.block = r.block; if (target.hp <= 0) target.alive = false; - if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, 0); + if (c.block) pBlock += c.block; + if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, c.block || 0); } else { pBlock += c.block || 0; if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0); diff --git a/tools/balance/sim-balance.test.mjs b/tools/balance/sim-balance.test.mjs index 1866805..6401afd 100644 --- a/tools/balance/sim-balance.test.mjs +++ b/tools/balance/sim-balance.test.mjs @@ -118,3 +118,16 @@ test('runBatch: 집계 필드·승률 범위', () => { test('runBatch: 동일 시드 동일 결과', () => { assert.deepEqual(runBatch(100, 7), runBatch(100, 7)); }); + +test('simulateCombat: 복합 카드(공격+방어) 블록이 적 공격을 흡수', () => { + const data = { + cards: { Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 1, block: 3 } }, + starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'], + monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 9 }] }], + }; + const r = simulateCombat(data, mulberry32(1)); + // 매 턴 3장(에너지3) → 블록 9 = 적 공격 9 전부 흡수 → 무피해로 MAX_TURNS 도달(draw), HP 유지. + // 블록 미적용이면 매턴 -9로 사망(win=false, draw 아님). + assert.equal(r.draw, true); + assert.equal(r.playerHpRemaining, 80); +}); diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 1c88cea..d5774b7 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -2430,6 +2430,9 @@ if c.kind == "Attack" then if c.damage ~= nil then self:PlayAttackFx(self.TargetIndex, c.image, 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 @@ -2653,16 +2656,14 @@ if anyAlive == false then self:TeleportToActMap() self:ShowMap() else - self:ShowResult("런 클리어!") - self.RunActive = false + self:EndRun("런 클리어!") end else self:OfferReward() end elseif self.PlayerHp <= 0 then self.CombatOver = true - self:ShowResult("패배...") - self.RunActive = false + self:EndRun("패배...") end`), method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} } local target = maps[self.Floor] @@ -2682,6 +2683,9 @@ 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('EndRun', `self:ShowResult(text) +self.RunActive = false +_TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) local m = self.Monsters[i] From 013f946c6b21e119a82993d1df5ca84a66077016 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 09:05:00 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(system-gaps):=20=EC=8B=A0=EA=B7=9C=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20RUID=20(?= =?UTF-8?q?=EC=9B=8C=20=EB=A6=AC=ED=94=84=C2=B7=EB=B8=8C=EB=9E=9C=EB=94=94?= =?UTF-8?q?=EC=8B=9C=20=E2=80=94=20=EB=A9=94=EC=9D=B4=EC=BB=A4=20=EC=84=A0?= =?UTF-8?q?=EB=B3=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- data/cards.json | 59 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/data/cards.json b/data/cards.json index 8946935..6a09f5a 100644 --- a/data/cards.json +++ b/data/cards.json @@ -1,10 +1,57 @@ { "cards": { - "Strike": { "name": "파워 스트라이크", "cost": 1, "kind": "Attack", "damage": 6, "desc": "피해 6", "image": "a71b116807904ef2b38e1dc013e2f9a2" }, - "Defend": { "name": "아이언 바디", "cost": 1, "kind": "Skill", "block": 5, "desc": "방어도 5", "image": "1ae9b6741c5947a8b528a0f515b50e3e" }, - "Bash": { "name": "슬래시 블러스트", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10", "image": "d5bc2953fcab4cfe9062af81c35aff86" }, - "WarLeap": { "name": "워 리프", "cost": 1, "kind": "Attack", "damage": 4, "block": 3, "desc": "피해 4, 방어도 3" }, - "Brandish": { "name": "브랜디시", "cost": 2, "kind": "Attack", "damage": 13, "desc": "피해 13" } + "Strike": { + "name": "파워 스트라이크", + "cost": 1, + "kind": "Attack", + "damage": 6, + "desc": "피해 6", + "image": "a71b116807904ef2b38e1dc013e2f9a2" + }, + "Defend": { + "name": "아이언 바디", + "cost": 1, + "kind": "Skill", + "block": 5, + "desc": "방어도 5", + "image": "1ae9b6741c5947a8b528a0f515b50e3e" + }, + "Bash": { + "name": "슬래시 블러스트", + "cost": 2, + "kind": "Attack", + "damage": 10, + "desc": "피해 10", + "image": "d5bc2953fcab4cfe9062af81c35aff86" + }, + "WarLeap": { + "name": "워 리프", + "cost": 1, + "kind": "Attack", + "damage": 4, + "block": 3, + "desc": "피해 4, 방어도 3", + "image": "992dabf6aff2400e92b2f4f705d8ebe7" + }, + "Brandish": { + "name": "브랜디시", + "cost": 2, + "kind": "Attack", + "damage": 13, + "desc": "피해 13", + "image": "21af4bccc5054a5dbc8245dfa7f08681" + } }, - "starterDeck": ["Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash"] + "starterDeck": [ + "Strike", + "Strike", + "Strike", + "Strike", + "Strike", + "Defend", + "Defend", + "Defend", + "Defend", + "Bash" + ] } From 134353d3742e626a86a106d668824c5fb21791ec Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 09:05:17 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat(system-gaps):=20=EC=82=B0=EC=B6=9C?= =?UTF-8?q?=EB=AC=BC=20=EC=9E=AC=EC=83=9D=EC=84=B1=20(=EA=B2=BD=EC=A0=9C?= =?UTF-8?q?=C2=B7=EB=B3=B5=ED=95=A9=EC=B9=B4=EB=93=9C=C2=B7EndRun)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- RootDesk/MyDesk/SlayDeckController.codeblock | 31 +++++++++++++++++--- ui/DefaultGroup.ui | 16 +++++----- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/RootDesk/MyDesk/SlayDeckController.codeblock b/RootDesk/MyDesk/SlayDeckController.codeblock index 5a46f89..c479e78 100644 --- a/RootDesk/MyDesk/SlayDeckController.codeblock +++ b/RootDesk/MyDesk/SlayDeckController.codeblock @@ -557,7 +557,7 @@ "Name": null }, "Arguments": [], - "Code": "self.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.Gold = 0\nself.Floor = 1\nself.RunLength = 3\nself.RunDeck = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself.RunActive = true\nself.RunRelics = {}\nself.Relics = {\n\tironHeart = { name = \"강철 심장\", desc = \"전투 시작 시 방어도 +6\", hook = \"combatStart\", effect = \"block\", value = 6 },\n\tenergyCore = { name = \"에너지 코어\", desc = \"턴 시작 시 에너지 +1\", hook = \"turnStart\", effect = \"energy\", value = 1 },\n\tvampire = { name = \"흡혈 송곳니\", desc = \"공격 카드 사용 시 HP +1\", hook = \"cardPlayed\", effect = \"healOnAttack\", value = 1 },\n\tgoldIdol = { name = \"황금 우상\", desc = \"전투 승리 시 골드 +10\", hook = \"combatReward\", effect = \"gold\", value = 10 },\n}\nself.RelicPool = { \"energyCore\", \"vampire\", \"goldIdol\" }\nself.Enemies = {\n\tslime = { name = \"슬라임\", maxHp = 45, intents = { { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 8 } } },\n\tslime_elite = { name = \"정예 슬라임\", maxHp = 70, intents = { { kind = \"Attack\", value = 14 }, { kind = \"Attack\", value = 8 }, { kind = \"Defend\", value = 10 } } },\n\tslime_boss = { name = \"슬라임 킹\", maxHp = 120, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 12 }, { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 22 } } },\n\torange_mushroom = { name = \"주황버섯\", maxHp = 16, intents = { { kind = \"Attack\", value = 5 }, { kind = \"Defend\", value = 4 }, { kind = \"Attack\", value = 7 } } },\n\tblue_mushroom = { name = \"파란버섯\", maxHp = 22, intents = { { kind = \"Attack\", value = 8 }, { kind = \"Attack\", value = 4 } } },\n\tpig = { name = \"돼지\", maxHp = 18, intents = { { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 3 } } },\n\tgreen_mushroom = { name = \"초록버섯\", maxHp = 20, intents = { { kind = \"Attack\", value = 7 }, { kind = \"Attack\", value = 4 } } },\n\tmushmom = { name = \"머쉬맘\", maxHp = 75, intents = { { kind = \"Attack\", value = 14 }, { kind = \"Defend\", value = 10 }, { kind = \"Attack\", value = 9 } } },\n\tmodified_snail = { name = \"변형된 달팽이\", maxHp = 60, intents = { { kind = \"Attack\", value = 12 }, { kind = \"Attack\", value = 7 }, { kind = \"Defend\", value = 8 } } },\n\tking_slime = { name = \"킹 슬라임\", maxHp = 130, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 14 }, { kind = \"Attack\", value = 12 }, { kind = \"Attack\", value = 24 } } },\n}\nself.MapNodes = {\n\tA = { type = \"combat\", enemy = \"slime\", row = 1, col = -1, next = { \"C\", \"D\" } },\n\tB = { type = \"combat\", enemy = \"slime\", row = 1, col = 1, next = { \"C\", \"D\" } },\n\tC = { type = \"rest\", row = 2, col = -1, next = { \"E\", \"F\" } },\n\tD = { type = \"shop\", row = 2, col = 1, next = { \"E\", \"F\" } },\n\tE = { type = \"elite\", enemy = \"slime_elite\", row = 3, col = -1, next = { \"BOSS\" } },\n\tF = { type = \"combat\", enemy = \"slime\", row = 3, col = 1, next = { \"BOSS\" } },\n\tBOSS = { type = \"boss\", enemy = \"slime_boss\", row = 4, col = 0, next = { } },\n}\nself.MapStart = { \"A\", \"B\" }\nself.CurrentNodeId = \"\"\nself.CurrentEnemyId = \"\"\nself:BindButtons()\nself:AddRelic(\"ironHeart\")\nself:ShowMap()", + "Code": "self.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.Gold = 0\nself.Floor = 1\nself.RunLength = 3\nself.RunDeck = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself.RunActive = true\nself.RunRelics = {}\nself.Relics = {\n\tironHeart = { name = \"강철 심장\", desc = \"전투 시작 시 방어도 +6\", hook = \"combatStart\", effect = \"block\", value = 6 },\n\tenergyCore = { name = \"에너지 코어\", desc = \"턴 시작 시 에너지 +1\", hook = \"turnStart\", effect = \"energy\", value = 1 },\n\tvampire = { name = \"흡혈 송곳니\", desc = \"공격 카드 사용 시 HP +1\", hook = \"cardPlayed\", effect = \"healOnAttack\", value = 1 },\n\tgoldIdol = { name = \"황금 우상\", desc = \"전투 승리 시 골드 +10\", hook = \"combatReward\", effect = \"gold\", value = 10 },\n}\nself.RelicPool = { \"energyCore\", \"vampire\", \"goldIdol\" }\nself.Enemies = {\n\tslime = { name = \"슬라임\", maxHp = 45, intents = { { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 8 } } },\n\tslime_elite = { name = \"정예 슬라임\", maxHp = 70, intents = { { kind = \"Attack\", value = 14 }, { kind = \"Attack\", value = 8 }, { kind = \"Defend\", value = 10 } } },\n\tslime_boss = { name = \"슬라임 킹\", maxHp = 120, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 12 }, { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 22 } } },\n\torange_mushroom = { name = \"주황버섯\", maxHp = 16, intents = { { kind = \"Attack\", value = 5 }, { kind = \"Attack\", value = 5 }, { kind = \"Defend\", value = 4 }, { kind = \"Attack\", value = 8 } } },\n\tblue_mushroom = { name = \"파란버섯\", maxHp = 22, intents = { { kind = \"Attack\", value = 4 }, { kind = \"Attack\", value = 4 }, { kind = \"Attack\", value = 10 } } },\n\tpig = { name = \"돼지\", maxHp = 18, intents = { { kind = \"Attack\", value = 6 }, { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 5 } } },\n\tgreen_mushroom = { name = \"초록버섯\", maxHp = 20, intents = { { kind = \"Attack\", value = 7 }, { kind = \"Defend\", value = 3 }, { kind = \"Attack\", value = 9 } } },\n\tmushmom = { name = \"머쉬맘\", maxHp = 75, intents = { { kind = \"Defend\", value = 10 }, { kind = \"Attack\", value = 16 }, { kind = \"Attack\", value = 9 }, { kind = \"Defend\", value = 6 } } },\n\tmodified_snail = { name = \"변형된 달팽이\", maxHp = 60, intents = { { kind = \"Attack\", value = 12 }, { kind = \"Defend\", value = 8 }, { kind = \"Attack\", value = 7 }, { kind = \"Attack\", value = 14 } } },\n\tking_slime = { name = \"킹 슬라임\", maxHp = 130, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 14 }, { kind = \"Attack\", value = 12 }, { kind = \"Attack\", value = 24 } } },\n}\nself.MapNodes = {\n\tA = { type = \"combat\", enemy = \"slime\", row = 1, col = -1, next = { \"C\", \"D\" } },\n\tB = { type = \"combat\", enemy = \"slime\", row = 1, col = 1, next = { \"C\", \"D\" } },\n\tC = { type = \"rest\", row = 2, col = -1, next = { \"E\", \"F\" } },\n\tD = { type = \"shop\", row = 2, col = 1, next = { \"E\", \"F\" } },\n\tE = { type = \"elite\", enemy = \"slime_elite\", row = 3, col = -1, next = { \"BOSS\" } },\n\tF = { type = \"combat\", enemy = \"slime\", row = 3, col = 1, next = { \"BOSS\" } },\n\tBOSS = { type = \"boss\", enemy = \"slime_boss\", row = 4, col = 0, next = { } },\n}\nself.MapStart = { \"A\", \"B\" }\nself.CurrentNodeId = \"\"\nself.CurrentEnemyId = \"\"\nself:BindButtons()\nself:AddRelic(\"ironHeart\")\nself:ShowMap()", "Scope": 2, "ExecSpace": 6, "Attributes": [], @@ -572,7 +572,7 @@ "Name": null }, "Arguments": [], - "Code": "self:ShowState(\"combat\")\nself:SetEntityEnabled(\"/ui/DefaultGroup/CombatHud/Result\", false)\nself.MaxEnergy = 3\nself.Turn = 0\nself.PlayerBlock = 0\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"파워 스트라이크\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6, image = \"a71b116807904ef2b38e1dc013e2f9a2\" },\n\tDefend = { name = \"아이언 바디\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5, image = \"1ae9b6741c5947a8b528a0f515b50e3e\" },\n\tBash = { name = \"슬래시 블러스트\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10, image = \"d5bc2953fcab4cfe9062af81c35aff86\" },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:BuildMonsters()\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()", + "Code": "self:ShowState(\"combat\")\nself:SetEntityEnabled(\"/ui/DefaultGroup/CombatHud/Result\", false)\nself.MaxEnergy = 3\nself.Turn = 0\nself.PlayerBlock = 0\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"파워 스트라이크\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6, image = \"a71b116807904ef2b38e1dc013e2f9a2\" },\n\tDefend = { name = \"아이언 바디\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5, image = \"1ae9b6741c5947a8b528a0f515b50e3e\" },\n\tBash = { name = \"슬래시 블러스트\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10, image = \"d5bc2953fcab4cfe9062af81c35aff86\" },\n\tWarLeap = { name = \"워 리프\", cost = 1, desc = \"피해 4, 방어도 3\", kind = \"Attack\", damage = 4, block = 3, image = \"992dabf6aff2400e92b2f4f705d8ebe7\" },\n\tBrandish = { name = \"브랜디시\", cost = 2, desc = \"피해 13\", kind = \"Attack\", damage = 13, image = \"21af4bccc5054a5dbc8245dfa7f08681\" },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:BuildMonsters()\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()", "Scope": 2, "ExecSpace": 6, "Attributes": [], @@ -1128,7 +1128,7 @@ "Name": "slot" } ], - "Code": "if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then\n\treturn\nend\nif self.Hand == nil then\n\treturn\nend\nlocal cardId = self.Hand[slot]\nif cardId == nil then\n\treturn\nend\nlocal c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nif self.Energy < c.cost then\n\tself:Toast(\"에너지가 부족합니다\")\n\treturn\nend\nself.Energy = self.Energy - c.cost\nif c.kind == \"Attack\" then\n\tif c.damage ~= nil then\n\t\tself:PlayAttackFx(self.TargetIndex, c.image, c.damage)\n\tend\n\tself:ApplyRelics(\"cardPlayed\")\nelseif c.kind == \"Skill\" then\n\tif c.block ~= nil then\n\t\tself.PlayerBlock = self.PlayerBlock + c.block\n\tend\nend\ntable.remove(self.Hand, slot)\ntable.insert(self.DiscardPile, cardId)\nself:RenderHand(false)\nself:RenderPiles()\nself:RenderCombat()\nself:CheckCombatEnd()", + "Code": "if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then\n\treturn\nend\nif self.Hand == nil then\n\treturn\nend\nlocal cardId = self.Hand[slot]\nif cardId == nil then\n\treturn\nend\nlocal c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nif self.Energy < c.cost then\n\tself:Toast(\"에너지가 부족합니다\")\n\treturn\nend\nself.Energy = self.Energy - c.cost\nif c.kind == \"Attack\" then\n\tif c.damage ~= nil then\n\t\tself:PlayAttackFx(self.TargetIndex, c.image, c.damage)\n\tend\n\tif c.block ~= nil then\n\t\tself.PlayerBlock = self.PlayerBlock + c.block\n\tend\n\tself:ApplyRelics(\"cardPlayed\")\nelseif c.kind == \"Skill\" then\n\tif c.block ~= nil then\n\t\tself.PlayerBlock = self.PlayerBlock + c.block\n\tend\nend\ntable.remove(self.Hand, slot)\ntable.insert(self.DiscardPile, cardId)\nself:RenderHand(false)\nself:RenderPiles()\nself:RenderCombat()\nself:CheckCombatEnd()", "Scope": 2, "ExecSpace": 6, "Attributes": [], @@ -1438,7 +1438,7 @@ "Name": null }, "Arguments": [], - "Code": "local anyAlive = false\nfor i = 1, #self.Monsters do\n\tif self.Monsters[i].alive == true then anyAlive = true; break end\nend\nif anyAlive == false then\n\tself.CombatOver = true\n\tself.Gold = self.Gold + 15\n\tself:ApplyRelics(\"combatReward\")\n\tself:RenderRun()\n\tlocal node = self.MapNodes[self.CurrentNodeId]\n\tif node ~= nil and node.type == \"elite\" then\n\t\tself:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)])\n\tend\n\tif node ~= nil and node.type == \"boss\" then\n\t\tif self.Floor < self.RunLength then\n\t\t\tself.Floor = self.Floor + 1\n\t\t\tself.CurrentNodeId = \"\"\n\t\t\tself.CurrentEnemyId = \"\"\n\t\t\tself:RenderRun()\n\t\t\tself:TeleportToActMap()\n\t\t\tself:ShowMap()\n\t\telse\n\t\t\tself:ShowResult(\"런 클리어!\")\n\t\t\tself.RunActive = false\n\t\tend\n\telse\n\t\tself:OfferReward()\n\tend\nelseif self.PlayerHp <= 0 then\n\tself.CombatOver = true\n\tself:ShowResult(\"패배...\")\n\tself.RunActive = false\nend", + "Code": "local anyAlive = false\nfor i = 1, #self.Monsters do\n\tif self.Monsters[i].alive == true then anyAlive = true; break end\nend\nif anyAlive == false then\n\tself.CombatOver = true\n\tself.Gold = self.Gold + 25\n\tself:ApplyRelics(\"combatReward\")\n\tself:RenderRun()\n\tlocal node = self.MapNodes[self.CurrentNodeId]\n\tif node ~= nil and node.type == \"elite\" then\n\t\tself.Gold = self.Gold + 15\n\t\tself:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)])\n\tend\n\tif node ~= nil and node.type == \"boss\" then\n\t\tif self.Floor < self.RunLength then\n\t\t\tself.Floor = self.Floor + 1\n\t\t\tself.CurrentNodeId = \"\"\n\t\t\tself.CurrentEnemyId = \"\"\n\t\t\tself:RenderRun()\n\t\t\tself:TeleportToActMap()\n\t\t\tself:ShowMap()\n\t\telse\n\t\t\tself:EndRun(\"런 클리어!\")\n\t\tend\n\telse\n\t\tself:OfferReward()\n\tend\nelseif self.PlayerHp <= 0 then\n\tself.CombatOver = true\n\tself:EndRun(\"패배...\")\nend", "Scope": 2, "ExecSpace": 6, "Attributes": [], @@ -1482,6 +1482,29 @@ "Attributes": [], "Name": "ShowResult" }, + { + "Return": { + "Type": "void", + "DefaultValue": null, + "SyncDirection": 0, + "Attributes": [], + "Name": null + }, + "Arguments": [ + { + "Type": "string", + "DefaultValue": null, + "SyncDirection": 0, + "Attributes": [], + "Name": "text" + } + ], + "Code": "self:ShowResult(text)\nself.RunActive = false\n_TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)", + "Scope": 2, + "ExecSpace": 6, + "Attributes": [], + "Name": "EndRun" + }, { "Return": { "Type": "void", diff --git a/ui/DefaultGroup.ui b/ui/DefaultGroup.ui index c8e1478..9e595fc 100644 --- a/ui/DefaultGroup.ui +++ b/ui/DefaultGroup.ui @@ -4034,7 +4034,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "파워 스트라이크", + "Text": "워 리프", "UseOutLine": true, "Enable": true } @@ -4222,7 +4222,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "피해 6", + "Text": "피해 4, 방어도 3", "UseOutLine": true, "Enable": true } @@ -4331,9 +4331,9 @@ "PreserveSprite": 0, "StartFrameIndex": 0, "Color": { - "r": 0.42, - "g": 0.55, - "b": 0.85, + "r": 0.86, + "g": 0.42, + "b": 0.38, "a": 1 }, "DropShadow": false, @@ -4602,7 +4602,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "1", + "Text": "2", "UseOutLine": true, "Enable": true } @@ -4790,7 +4790,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "아이언 바디", + "Text": "브랜디시", "UseOutLine": true, "Enable": true } @@ -4978,7 +4978,7 @@ "bottom": 0 }, "SizeFit": false, - "Text": "방어도 5", + "Text": "피해 13", "UseOutLine": true, "Enable": true }