docs(E6a): 다음 막/멀티 act 설계·구현 계획

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 04:13:56 +09:00
parent f42628c2e9
commit 376511dfa9
2 changed files with 270 additions and 0 deletions

View File

@@ -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 사용.

View File

@@ -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). 막별 다른 맵 디자인·신규 적/배경·막별 보상 차등.