From f42628c2e99c82682aec3f82725ba7d2f2d42196 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 9 Jun 2026 04:13:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(E6a):=20=EB=8B=A4=EC=9D=8C=20=EB=A7=89?= =?UTF-8?q?=20=EC=A7=84=ED=96=89=C2=B7=EC=A0=81=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=9D=BC=20=E2=80=94=20=EB=A9=80=ED=8B=B0=20act=20=EB=9F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보스 클리어 시 즉시 종료 대신 다음 막으로, 최종 막 보스에서 런 클리어. - Floor를 막 카운터(1..ACT_COUNT=3)로 재정의, RunLength=ACT_COUNT - StartCombat: 적을 막 배율(mult=1+(Floor-1)*0.6)로 스케일(maxHp·의도값, 새 테이블) - CheckCombatEnd 보스 승리: Floor --- RootDesk/MyDesk/SlayDeckController.codeblock | 8 ++--- tools/gen-slaydeck.mjs | 31 +++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/RootDesk/MyDesk/SlayDeckController.codeblock b/RootDesk/MyDesk/SlayDeckController.codeblock index 1a61a28..dc05125 100644 --- a/RootDesk/MyDesk/SlayDeckController.codeblock +++ b/RootDesk/MyDesk/SlayDeckController.codeblock @@ -314,7 +314,7 @@ "Name": null }, "Arguments": [], - "Code": "self.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.Gold = 0\nself.Floor = 0\nself.RunLength = 4\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}\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}\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": [], @@ -329,7 +329,7 @@ "Name": null }, "Arguments": [], - "Code": "self.MaxEnergy = 3\nself.Turn = 0\nlocal node = self.MapNodes[self.CurrentNodeId]\nif node ~= nil then\n\tself.Floor = node.row\nend\nlocal enemy = self.Enemies[self.CurrentEnemyId]\nself.PlayerBlock = 0\nself.EnemyName = enemy.name\nself.EnemyMaxHp = enemy.maxHp\nself.EnemyHp = self.EnemyMaxHp\nself.EnemyBlock = 0\nself.EnemyIntents = enemy.intents\nself.EnemyIntentIndex = 1\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"타격\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6 },\n\tDefend = { name = \"방어\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5 },\n\tBash = { name = \"강타\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10 },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()", + "Code": "self.MaxEnergy = 3\nself.Turn = 0\nlocal enemy = self.Enemies[self.CurrentEnemyId]\nlocal mult = 1 + (self.Floor - 1) * 0.6\nself.PlayerBlock = 0\nself.EnemyName = enemy.name\nself.EnemyMaxHp = math.floor(enemy.maxHp * mult)\nself.EnemyHp = self.EnemyMaxHp\nself.EnemyBlock = 0\nself.EnemyIntents = {}\nfor i = 1, #enemy.intents do\n\tself.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) }\nend\nself.EnemyIntentIndex = 1\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"타격\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6 },\n\tDefend = { name = \"방어\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5 },\n\tBash = { name = \"강타\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10 },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()", "Scope": 2, "ExecSpace": 6, "Attributes": [], @@ -699,7 +699,7 @@ "Name": null }, "Arguments": [], - "Code": "if self.EnemyHp <= 0 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\tself:ShowResult(\"런 클리어!\")\n\t\tself.RunActive = false\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": "if self.EnemyHp <= 0 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: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", "Scope": 2, "ExecSpace": 6, "Attributes": [], @@ -752,7 +752,7 @@ "Name": null }, "Arguments": [], - "Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/Floor\", \"층 \" .. string.format(\"%d\", self.Floor) .. \"/\" .. string.format(\"%d\", self.RunLength))\nself:SetText(\"/ui/DefaultGroup/CombatHud/Gold\", \"골드 \" .. string.format(\"%d\", self.Gold))", + "Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/Floor\", \"막 \" .. string.format(\"%d\", self.Floor) .. \"/\" .. string.format(\"%d\", self.RunLength))\nself:SetText(\"/ui/DefaultGroup/CombatHud/Gold\", \"골드 \" .. string.format(\"%d\", self.Gold))", "Scope": 2, "ExecSpace": 6, "Attributes": [], diff --git a/tools/gen-slaydeck.mjs b/tools/gen-slaydeck.mjs index 0d8757f..236ac88 100644 --- a/tools/gen-slaydeck.mjs +++ b/tools/gen-slaydeck.mjs @@ -935,6 +935,7 @@ function writeCodeblocks() { const CARD_PRICE = 30; const REST_HEAL = 30; const RELIC_PRICE = 60; + const ACT_COUNT = 3; const combat = codeblock('SlayDeckController', 'SlayDeckController', [ prop('any', 'DrawPile'), prop('any', 'DiscardPile'), @@ -978,8 +979,8 @@ function writeCodeblocks() { method('StartRun', `self.PlayerMaxHp = 80 self.PlayerHp = self.PlayerMaxHp self.Gold = 0 -self.Floor = 0 -self.RunLength = ${MAX_ROW} +self.Floor = 1 +self.RunLength = ${ACT_COUNT} self.RunDeck = { ${CARDS.starterDeck.map(luaStr).join(', ')} } self.RunActive = true self.RunRelics = {} @@ -995,17 +996,17 @@ self:AddRelic("${RELICS.startingRelic}") self:ShowMap()`), method('StartCombat', `self.MaxEnergy = 3 self.Turn = 0 -local node = self.MapNodes[self.CurrentNodeId] -if node ~= nil then - self.Floor = node.row -end local enemy = self.Enemies[self.CurrentEnemyId] +local mult = 1 + (self.Floor - 1) * 0.6 self.PlayerBlock = 0 self.EnemyName = enemy.name -self.EnemyMaxHp = enemy.maxHp +self.EnemyMaxHp = math.floor(enemy.maxHp * mult) self.EnemyHp = self.EnemyMaxHp self.EnemyBlock = 0 -self.EnemyIntents = enemy.intents +self.EnemyIntents = {} +for i = 1, #enemy.intents do + self.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) } +end self.EnemyIntentIndex = 1 self.CombatOver = false self.DiscardPile = {} @@ -1268,8 +1269,16 @@ self:RenderCombat()`), self:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)]) end if node ~= nil and node.type == "boss" then - self:ShowResult("런 클리어!") - self.RunActive = false + if self.Floor < self.RunLength then + self.Floor = self.Floor + 1 + self.CurrentNodeId = "" + self.CurrentEnemyId = "" + self:RenderRun() + self:ShowMap() + else + self:ShowResult("런 클리어!") + self.RunActive = false + end else self:OfferReward() end @@ -1299,7 +1308,7 @@ self:SetText("/ui/DefaultGroup/CombatHud/EnemyIntent", intentText) 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:RenderRun()`), - method('RenderRun', `self:SetText("/ui/DefaultGroup/CombatHud/Floor", "층 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength)) + method('RenderRun', `self:SetText("/ui/DefaultGroup/CombatHud/Floor", "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength)) self:SetText("/ui/DefaultGroup/CombatHud/Gold", "골드 " .. string.format("%d", self.Gold))`), method('OfferReward', `local pool = {} for id, _ in pairs(self.Cards) do -- 2.49.1 From 376511dfa92d73882902f3046bcd120152f0da3e Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 9 Jun 2026 04:13:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(E6a):=20=EB=8B=A4=EC=9D=8C=20=EB=A7=89?= =?UTF-8?q?/=EB=A9=80=ED=8B=B0=20act=20=EC=84=A4=EA=B3=84=C2=B7=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EA=B3=84=ED=9A=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/superpowers/plans/2026-06-09-floors.md | 206 ++++++++++++++++++ .../specs/2026-06-09-floors-design.md | 64 ++++++ 2 files changed, 270 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-floors.md create mode 100644 docs/superpowers/specs/2026-06-09-floors-design.md diff --git a/docs/superpowers/plans/2026-06-09-floors.md b/docs/superpowers/plans/2026-06-09-floors.md new file mode 100644 index 0000000..5cb36e8 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-floors.md @@ -0,0 +1,206 @@ +# 다음 층 / 멀티 act (TODO E6a) 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:** 보스 클리어 시 즉시 종료 대신 다음 막으로 진행(적 스케일), 최종 막 보스에서 진짜 런 클리어. + +**Architecture:** `Floor`를 막 카운터로 재정의(1..ACT_COUNT). StartCombat에서 적을 막 배율로 스케일, CheckCombatEnd 보스 승리 시 다음 막(같은 맵 재사용)으로. 모두 `gen-slaydeck.mjs`에서 생성. + +**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock. 검증은 node --check+재생성+결정성+메이커 Play. + +--- + +## File Structure +- Modify: `tools/gen-slaydeck.mjs` — ACT_COUNT 상수, StartRun(Floor=1·RunLength=ACT_COUNT), StartCombat(Floor 제거·적 스케일), CheckCombatEnd(보스 다음 막), RenderRun(막 라벨). + +검증: MSW Lua 단위테스트 불가 → 생성기 문법·재생성·결정성·메이커 Play. + +--- + +### Task 1: ACT_COUNT 상수 + StartRun + +**Files:** Modify `tools/gen-slaydeck.mjs` + +- [ ] **Step 1: ACT_COUNT 상수** — `const RELIC_PRICE = 60;` 다음에: + +```js + const ACT_COUNT = 3; +``` + +- [ ] **Step 2: StartRun의 Floor·RunLength 변경** — StartRun 코드에서 아래 두 줄을 교체: + +기존: +``` +self.Floor = 0 +self.RunLength = ${MAX_ROW} +``` +신규: +``` +self.Floor = 1 +self.RunLength = ${ACT_COUNT} +``` + +- [ ] **Step 3: 문법 검사** + +Run: `node --check tools/gen-slaydeck.mjs` +Expected: 오류 없음 + +- [ ] **Step 4: 커밋** + +```bash +git add tools/gen-slaydeck.mjs +git commit -m "gen-slaydeck(E6a): ACT_COUNT·StartRun 막 카운터 초기화" +``` + +--- + +### Task 2: StartCombat — Floor 제거 + 적 막 스케일 + +**Files:** Modify `tools/gen-slaydeck.mjs` + +- [ ] **Step 1: Floor=node.row 블록 제거 + 적 스케일 적용** — StartCombat 코드의 아래 블록을 교체: + +기존: +``` +local node = self.MapNodes[self.CurrentNodeId] +if node ~= nil then + self.Floor = node.row +end +local enemy = self.Enemies[self.CurrentEnemyId] +self.PlayerBlock = 0 +self.EnemyName = enemy.name +self.EnemyMaxHp = enemy.maxHp +self.EnemyHp = self.EnemyMaxHp +self.EnemyBlock = 0 +self.EnemyIntents = enemy.intents +self.EnemyIntentIndex = 1 +``` +신규: +``` +local enemy = self.Enemies[self.CurrentEnemyId] +local mult = 1 + (self.Floor - 1) * 0.6 +self.PlayerBlock = 0 +self.EnemyName = enemy.name +self.EnemyMaxHp = math.floor(enemy.maxHp * mult) +self.EnemyHp = self.EnemyMaxHp +self.EnemyBlock = 0 +self.EnemyIntents = {} +for i = 1, #enemy.intents do + self.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) } +end +self.EnemyIntentIndex = 1 +``` + +- [ ] **Step 2: 문법 검사** + +Run: `node --check tools/gen-slaydeck.mjs` +Expected: 오류 없음 + +- [ ] **Step 3: 커밋** + +```bash +git add tools/gen-slaydeck.mjs +git commit -m "gen-slaydeck(E6a): StartCombat 적 막 스케일·Floor 제거" +``` + +--- + +### Task 3: CheckCombatEnd 보스 다음 막 + RenderRun 막 라벨 + +**Files:** Modify `tools/gen-slaydeck.mjs` + +- [ ] **Step 1: CheckCombatEnd 보스 분기 교체** — 아래 블록을 교체: + +기존: +``` + if node ~= nil and node.type == "boss" then + self:ShowResult("런 클리어!") + self.RunActive = false + else + self:OfferReward() + end +``` +신규: +``` + if node ~= nil and node.type == "boss" then + if self.Floor < self.RunLength then + self.Floor = self.Floor + 1 + self.CurrentNodeId = "" + self.CurrentEnemyId = "" + self:RenderRun() + self:ShowMap() + else + self:ShowResult("런 클리어!") + self.RunActive = false + end + else + self:OfferReward() + end +``` + +- [ ] **Step 2: RenderRun 막 라벨** — RenderRun의 Floor 텍스트 줄을 교체: + +기존: +``` +self:SetText("/ui/DefaultGroup/CombatHud/Floor", "층 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength)) +``` +신규: +``` +self:SetText("/ui/DefaultGroup/CombatHud/Floor", "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength)) +``` + +- [ ] **Step 3: 문법 검사** + +Run: `node --check tools/gen-slaydeck.mjs` +Expected: 오류 없음 + +- [ ] **Step 4: 커밋** + +```bash +git add tools/gen-slaydeck.mjs +git commit -m "gen-slaydeck(E6a): 보스 승리 다음 막 진행·막 라벨" +``` + +--- + +### Task 4: 재생성 + 검증 + +**Files:** 생성물 + +- [ ] **Step 1: 생성** + +Run: `node tools/gen-slaydeck.mjs` +Expected: `Slay deck UI and combat codeblocks generated.` + +- [ ] **Step 2: 스케일·막 진행 코드 확인** + +Run: `node -e "const j=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8')); const sc=j.ContentProto.Json.Methods.find(m=>m.Name==='StartCombat').Code; console.log(/mult = 1 \+ \(self.Floor - 1\) \* 0.6/.test(sc)&&/math.floor\(enemy.maxHp \* mult\)/.test(sc)?'SCALE OK':'NO SCALE'); const cc=j.ContentProto.Json.Methods.find(m=>m.Name==='CheckCombatEnd').Code; console.log(/self.Floor = self.Floor \+ 1/.test(cc)?'NEXT-ACT OK':'NO NEXT-ACT')"` +Expected: `SCALE OK` / `NEXT-ACT OK` + +- [ ] **Step 3: 결정성** + +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: git status** + +Run: `git checkout -- Global/common.gamelogic 2>/dev/null; git status --short` +Expected: `tools/gen-slaydeck.mjs`, `ui/DefaultGroup.ui`, `RootDesk/MyDesk/SlayDeckController.codeblock` (+docs). (data 변경 없음) + +- [ ] **Step 5: 생성물 커밋** + +```bash +git add ui/DefaultGroup.ui RootDesk/MyDesk/SlayDeckController.codeblock +git commit -m "재생성(E6a): 멀티 act·적 스케일 반영" +``` + +- [ ] **Step 6: 메이커 Play 수동 검증 (MCP)** + +reload→Play: 1막 보스(슬라임 킹 120) 처치 → 2막 맵(Floor 2)·적 HP 스케일(슬라임 72·보스 192) → 3막 보스 처치 → "런 클리어!". HP/골드/덱/유물 막 간 유지. MCP는 PickNode/PlayCard/CheckCombatEnd 직접 호출 + 로그. + +--- + +## Self-Review +- **Spec coverage:** ACT_COUNT·StartRun(Task1), StartCombat 스케일·Floor제거(Task2), 보스 다음막·막라벨(Task3), 검증(Task4). 스펙 전 항목 매핑. +- **Placeholder scan:** 모든 단계 실제 코드/명령. +- **Type consistency:** `Floor`(막 카운터)·`RunLength`(=ACT_COUNT)·`mult` 사용 일관. `EnemyIntents` 새 테이블 생성(공유 변형 없음). CheckCombatEnd의 `node`는 기존 정의 사용. ACT_COUNT 상수 Task1 정의·Task1·3 사용. diff --git a/docs/superpowers/specs/2026-06-09-floors-design.md b/docs/superpowers/specs/2026-06-09-floors-design.md new file mode 100644 index 0000000..141e325 --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-floors-design.md @@ -0,0 +1,64 @@ +# 다음 층 / 멀티 act (TODO E6a) — 설계 + +> 작성: 2026-06-09 / 상태: 승인됨 / 근거: TODO E 분해(E6a) + 기존 맵/전투 구조. +> 선행: E1~E5 완료. 제외: E6b 저장/불러오기(사용자가 안 함으로 결정 — MSW 저장 API 필요). + +## 문제 + +현재 보스 클리어 = 즉시 런 종료. 로그라이크의 "여러 층(act)을 점점 깊이 진행" 느낌이 없다. +보스 클리어 후 다음 막으로 이어지고, 최종 막 보스에서 진짜 런 클리어가 필요하다. + +## 설계 + +### 파라미터 (생성기 상수) +- `ACT_COUNT = 3` (막 수). +- 적 스케일: `mult = 1 + (Act-1)*0.6` → 1막 ×1, 2막 ×1.6, 3막 ×2.2. + +### 상태 재정의 +- 기존 `Floor`를 **현재 막 카운터**(1..ACT_COUNT)로 사용. `RunLength = ACT_COUNT`. +- 맵 내 행 진행은 맵 UI가 표현 → 별도 숫자 표시 없음. + +### 메서드 변경 +- `StartRun`: `Floor = 1`, `RunLength = ${ACT_COUNT}`. (맵 1회 빌드는 그대로.) +- `StartCombat`: `self.Floor = node.row` 줄 **제거**. 적 로드 시 막 스케일 적용: + ```lua + local mult = 1 + (self.Floor - 1) * 0.6 + self.EnemyMaxHp = math.floor(enemy.maxHp * mult) + self.EnemyHp = self.EnemyMaxHp + self.EnemyIntents = {} + for i = 1, #enemy.intents do + self.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) } + end + ``` + (공유 enemy.intents 변형 금지 — 새 테이블 생성.) +- `CheckCombatEnd` 보스 승리 분기: + ```lua + if node ~= nil and node.type == "boss" then + if self.Floor < self.RunLength then + self.Floor = self.Floor + 1 + self.CurrentNodeId = "" + self.CurrentEnemyId = "" + self:RenderRun() + self:ShowMap() + else + self:ShowResult("런 클리어!") + self.RunActive = false + end + else + self:OfferReward() + end + ``` + (다음 막은 같은 맵 구조 재사용 — CurrentNodeId 리셋만. 적은 막 스케일로 강해짐.) +- HP·골드·덱·유물은 막 간 유지(기존 영속). combatStart 유물은 전투마다 재적용. + +### UI +- `RenderRun`: 층 텍스트를 `"막 " .. Floor .. "/" .. RunLength`로 (라벨 "층"→"막"). 골드 표시 유지. + +## 검증 (메이커 Play) +- 1막 보스(슬라임 킹 120) 처치 → 2막 맵·Floor 2 → 적 HP 스케일(슬라임 45→72, 보스 120→192). +- 3막 보스 처치 → "런 클리어!". HP/골드/덱/유물 막 간 유지. +- 패배 시 종료. 생성기 결정적·JSON 유효. +- (버튼 런타임 — MCP는 PickNode/PlayCard/CheckCombatEnd 직접 호출 + 상태 로그.) + +## 범위 밖 (금지) +- 저장/불러오기(E6b). 막별 다른 맵 디자인·신규 적/배경·막별 보상 차등. -- 2.49.1