diff --git a/docs/superpowers/plans/2026-06-16-generator-modularization.md b/docs/superpowers/plans/2026-06-16-generator-modularization.md new file mode 100644 index 0000000..aaacccc --- /dev/null +++ b/docs/superpowers/plans/2026-06-16-generator-modularization.md @@ -0,0 +1,169 @@ +# 생성기 모듈화 (Phase 1) 구현 계획 + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans 로 태스크 단위 구현. 단계는 `- [ ]` 체크박스. + +**Goal:** `tools/deck/gen-slaydeck.mjs`(~6,200줄)의 공유 인프라와 UI emit 16종을 `lib/`·`hud/` 모듈로 분리하되 출력 산출물은 바이트 동일로 유지한다. + +**Architecture:** 단방향 의존 — `gen-slaydeck.mjs`(오케스트레이터) → `hud/*.mjs`(HUD별 build 함수) → `lib/*.mjs`(헬퍼·상수·데이터). `guid(prefix,n)`가 순수 함수라 모듈화해도 emit 순서만 보존하면 출력 불변. codeblock 메서드는 이번 범위 제외. + +**Tech Stack:** Node ESM. 검증 = **바이트 동일 재생성**(git diff 빈 결과) + 미러 `node --test`. + +--- + +## 🔑 검증 게이트 (모든 Task 공통) + +각 추출 후 반드시: +``` +node tools/deck/gen-slaydeck.mjs # 성공 메시지 1줄, throw 없음 +git status --short +``` +**합격 기준**: `ui/DefaultGroup.ui`·`RootDesk/MyDesk/SlayDeckController.codeblock`이 **변경 안 됨**(git status에 안 뜸). `Global/common.gamelogic`만 ` M`이면 LF churn → `git checkout -- Global/common.gamelogic`. +- 만약 ui/codeblock이 ` M`로 뜨면 **추출 중 실수**(참조 누락/순서 변경) → `git diff --stat`로 어느 산출물인지 보고 되돌려 원인 수정. (RULES상 산출물 content는 안 봄 — 소스 diff로 원인 파악.) + +--- + +## 파일 구조 (목표) +``` +tools/deck/ + gen-slaydeck.mjs # 오케스트레이터: import → 데이터 로드(lib) → upsertUi(hud 호출) → writeCodeblocks → patchCommon + lib/ + data.mjs # 데이터 로드·검증·luaXxxTable·frameRuid·게임상수 + ui-helpers.mjs # guid/transform/sprite/button/text/entity/scrollLayoutGroup/cardFaceLayout/applySortingOverride + # + UI 상수 + uiPath/sectionRoot/isGeneratedUiEntity/appendUiSection + hud/ + deckhud.mjs deckinspect.mjs deckall.mjs combat.mjs reward.mjs map.mjs + shop.mjs rest.mjs treasure.mjs jobchoice.mjs jobselect.mjs mainmenu.mjs + charselect.mjs lobby.mjs board.mjs soulshop.mjs +``` + +--- + +### Task 1: `lib/data.mjs` — 데이터·게임상수·lua 테이블 추출 + +**Files:** Create `tools/deck/lib/data.mjs`; Modify `tools/deck/gen-slaydeck.mjs`(상단 데이터/lua 블록 → import) + +- [ ] **Step 1:** `lib/data.mjs` 생성. gen-slaydeck.mjs에서 아래를 **잘라 이동**(정의 본문 그대로): + - 데이터 로드+검증: `CARDS`(:3) `ENEMIES`(:4) `CLASSES`(:7~17) `JOBS`(:19~40) `SOUL_UNLOCKS`(:42~47) `CARDFRAMES`+검증(:57~68) `RARITIES`(:58) `NODEICONS`+검증(:92~96) `CHARS`+검증(:99~103) `CAM`(:105) `RELICS`+검증(:107~) `POTIONS`+검증(:118~) + - 게임 상수: `MAP_ROWS`(:84) `MAP_COLS`(:85) `CHEST_CLOSED_RUID`(:88) `CHEST_OPEN_RUID`(:89) + - 함수: `luaSoulShopTable`(:48) `frameRuid`(:69) `luaFramesTable`(:72) `luaNodeIconsTable`(:78) `luaRelicsTable` `luaPotionsTable` `luaIntentsArray` `luaEnemiesTable` `luaStr` `luaJobsTable` `luaCardsTable` `luaDeckTable` + - 맨 위 `import { readFileSync } from 'node:fs';`, 맨 끝 `export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, NODEICONS, CHARS, CAM, RELICS, POTIONS, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable };` + - ⚠️ `luaNodeIconsTable`는 `luaStr`를 쓰므로 `luaStr`도 같이 이동. `luaFramesTable`도 `luaStr` 사용. 상호 참조는 같은 모듈 내라 OK. + +- [ ] **Step 2:** gen-slaydeck.mjs 상단에 `import { CARDS, ENEMIES, ... , luaDeckTable } from './lib/data.mjs';` 추가(이동한 정의 위치에). + +- [ ] **Step 3:** 검증 게이트 실행 → ui/codeblock 0 변경 확인 → common.gamelogic churn 복원. + +- [ ] **Step 4:** 커밋 +``` +git add tools/deck/lib/data.mjs tools/deck/gen-slaydeck.mjs +git commit -m "refactor(gen): lib/data.mjs로 데이터·lua 테이블 추출 (출력 불변)" +``` + +--- + +### Task 2: `lib/ui-helpers.mjs` — UI 헬퍼·상수 추출 + +**Files:** Create `tools/deck/lib/ui-helpers.mjs`; Modify `tools/deck/gen-slaydeck.mjs` + +- [ ] **Step 1:** `lib/ui-helpers.mjs` 생성. gen-slaydeck.mjs에서 이동: + - UI 상수: `UI_FILE`(:190) `COMMON_FILE`(:191) `UI_ROOT`(:192) `GENERATED_UI_SECTIONS`(:193) `UI_APPEND_ORDER`(:211) `DISABLED_STOCK_CONTROLS`(:229) `TRANSPARENT DARK GOLD ATTACK DEFEND SKILL`(:231~236) `DAMAGE_DIGIT_RUIDS`(:237) `DAMAGE_POP_*`(:249~252) `MAX_MONSTERS`(:254) `HEAD_OFFSET_Y`(:255) `HP_BAR_W`(:257) `WHITE`(:258) `CARD_NAME_TEXT CARD_DESC_TEXT`(:259~260) `CARD_W CARD_H CARD_SPACING CARD_XS`(:276~279) `ALIGN_CENTER ALIGN_BOTTOM_CENTER`(:281~282) + - 헬퍼: `cardFaceLayout`(:264) `guid`(:284) `transform`(:292) `sprite`(:317) `button`(:353) `text`(:378) `scrollLayoutGroup`(:405) `popupLayerFor`(:437) `uiOrderFor`(:443) `displayOrderFor`(:452) `applySortingOverride`(:456) `entity`(:472) `uiPath`(:504) `sectionRoot`(:508) `isGeneratedUiEntity`(:512) `appendUiSection`(:516) + - `export { ... }` 전부. + - ⚠️ COMMON_FILE은 patchCommon(:6125)도 사용 → export 필요. UI_APPEND_ORDER·GENERATED_UI_SECTIONS는 upsertUi가 사용. + +- [ ] **Step 2:** gen-slaydeck.mjs에 `import { ... } from './lib/ui-helpers.mjs';` 추가. + +- [ ] **Step 3:** 검증 게이트 → 0 변경 → churn 복원. + +- [ ] **Step 4:** 커밋 `git commit -m "refactor(gen): lib/ui-helpers.mjs로 UI 헬퍼·상수 추출 (출력 불변)"` + +--- + +### HUD 추출 공통 레시피 (Task 3~6에 반복 적용) + +각 HUD는 현재 `upsertUi()` 안에서 `const = []; const add = (e) => .push(e); add(entity(...)); …; emit('', );` 형태다. 추출 절차: +1. `tools/deck/hud/.mjs` 생성: `import { guid, entity, transform, sprite, button, text, GOLD, ... } from '../lib/ui-helpers.mjs';` + 필요한 데이터는 `'../lib/data.mjs'`. `export function build() { const e = []; const add = (x)=>e.push(x); add(entity(...)); …; return e; }` — **본문은 기존 라인 그대로 이동**(emit 호출 줄만 제외). +2. upsertUi에서 해당 블록을 `emit('', build());` 한 줄로 치환. +3. **검증 게이트**(ui/codeblock 0 변경) → churn 복원 → 커밋. +4. ⚠️ 옮긴 블록이 upsertUi 지역변수(`byPath`/`ui`/`cards`/`previewIds`)를 참조하면 안 됨(HUD 섹션은 헬퍼·데이터만 씀이 확인됨). 참조 시 바이트 diff로 즉시 드러남 → 그 변수를 인자로 받도록 조정. + +추출 대상(순서·소스 라인·emit명·모듈): + +| 모듈 | emit | 소스(블록 시작~emit줄) | +|---|---|---| +| `hud/deckhud.mjs` → `buildDeckHud` | DeckHud | `:693`(`const hud=[]`)~`:808` | +| `hud/deckinspect.mjs` → `buildDeckInspect` | DeckInspectHud | `:810`~`:942` | +| `hud/deckall.mjs` → `buildDeckAll` | DeckAllHud | `:944`~`:1097` | +| `hud/combat.mjs` → `buildCombat` | CombatHud | `:1100`~`:1587` | +| `hud/reward.mjs` → `buildReward` | RewardHud | `:1589`~`:1681` | +| `hud/map.mjs` → `buildMap` | MapHud | `:1684`~`:1839` | +| `hud/shop.mjs` → `buildShop` | ShopHud | `:1841`~`:2038` | +| `hud/rest.mjs` → `buildRest` | RestHud | `:2040`~`:2095` | +| `hud/treasure.mjs` → `buildTreasure` | TreasureHud | `:2098`~`:2181` | +| `hud/jobchoice.mjs` → `buildJobChoice` | JobChoiceHud | `:2184`~`:2229` | +| `hud/jobselect.mjs` → `buildJobSelect` | JobSelectHud | `:2231`~`:2314` | +| `hud/mainmenu.mjs` → `buildMainMenu` | MainMenu | `:2316`~`:2616` | +| `hud/charselect.mjs` → `buildCharSelect` | CharacterSelectHud | `:2437`~`:2617`(`select[0]…enable=false` 포함) | +| `hud/lobby.mjs` → `buildLobby` | LobbyHud | `:2620`~`:2672` | +| `hud/board.mjs` → `buildBoard` | BoardHud | `:2675`~`:2727` | +| `hud/soulshop.mjs` → `buildSoulShop` | SoulShopHud | `:2729`~`:2814` | + +⚠️ MainMenu/CharacterSelectHud는 `const menu=[]`(:2316)·`const select=[]`(:2437)로 인접 정의 후 `emit('MainMenu', menu); emit('CharacterSelectHud', select);`가 :2616~2617에 연속. 각각 별 모듈로 분리, emit 두 줄로. `select[0].jsonString.enable=false`(:2596)는 buildCharSelect 내부에서 `e[0].jsonString.enable=false`로. +⚠️ **CardHand 스톡카드 in-place upsert(`:557~691`)는 추출 안 함** — 기존 .ui 엔티티를 변형하는 특수 로직이라 upsertUi에 잔류(import만 정리). + +--- + +### Task 3: HUD 추출 배치 A (말단부터 — 위험 낮은 순) +**Files:** Create `hud/soulshop.mjs board.mjs lobby.mjs charselect.mjs`; Modify gen-slaydeck.mjs + +- [ ] **Step 1~4:** 레시피로 **soulshop → board → lobby → charselect** 순서로 하나씩 추출·검증·커밋(HUD 1개당 커밋 1개 권장, 배치로 묶어도 무방). 각 추출 후 검증 게이트 통과 필수. + - charselect는 P15/이번 작업으로 검증된 화면이라 패턴 안정. `e[0].jsonString.enable=false` 처리 확인. + +--- + +### Task 4: HUD 추출 배치 B +**Files:** Create `hud/mainmenu.mjs jobselect.mjs jobchoice.mjs treasure.mjs rest.mjs`; Modify gen-slaydeck.mjs + +- [ ] **Step 1~4:** 레시피로 **mainmenu → jobselect → jobchoice → treasure → rest** 추출·검증·커밋. + +--- + +### Task 5: HUD 추출 배치 C +**Files:** Create `hud/shop.mjs map.mjs reward.mjs`; Modify gen-slaydeck.mjs + +- [ ] **Step 1~4:** 레시피로 **shop → map → reward** 추출·검증·커밋. + +--- + +### Task 6: HUD 추출 배치 D (대형 — 마지막) +**Files:** Create `hud/combat.mjs deckall.mjs deckinspect.mjs deckhud.mjs`; Modify gen-slaydeck.mjs + +- [ ] **Step 1~4:** 레시피로 **deckhud → deckinspect → deckall → combat** 추출·검증·커밋. combat(~487줄)이 가장 크니 마지막. 추출 후 upsertUi는 데이터 준비 + CardHand upsert + `emit('X', buildX())` 16줄 + 병합만 남아야 함. + +--- + +### Task 7: 마무리 — RULES 동기화 + 회귀 + PR + +**Files:** Modify `RULES.md` + +- [ ] **Step 1:** RULES.md §1 보조 생성기/단일소스 표에 반영: `tools/deck/gen-slaydeck.mjs`(오케스트레이터)·`tools/deck/lib/*.mjs`(공유)·`tools/deck/hud/*.mjs`(HUD별)가 함께 `ui/DefaultGroup.ui`·`SlayDeckController.codeblock`의 단일 소스임을 명시. + +- [ ] **Step 2:** 회귀 테스트(무영향 확인) +``` +node --test tools/balance/sim-balance.test.mjs +node --test tools/map/rogue-map.test.mjs +``` +Expected: 전부 pass(37/0, 9/0). + +- [ ] **Step 3:** 최종 재생성 + 전체 검증 게이트 → ui/codeblock 0 변경(누적) 최종 확인. `git status --short`에 산출물 변경 없음. + +- [ ] **Step 4:** RULES 커밋 → push → PR(`node tools/git/gitea-pr.mjs create `, UTF-8). + +--- + +## Self-Review + +- **스펙 커버리지**: lib/data·ui-helpers(T1,T2) · HUD 16종 모듈화(T3~6) · 바이트 동일 게이트(공통 게이트, 매 Task) · codeblock 제외(범위 명시) · RULES 동기화(T7) · 미러 회귀(T7). 누락 없음. +- **플레이스홀더**: 이동 대상은 라인 범위로 구체 지정, 검증 명령·합격기준 명시, export/import 목록 구체. "본문 그대로 이동"은 리팩터 특성상 코드 재타이핑 대신 정확한 소스 위치 지정(바이트 검증이 정확성 보장). +- **타입 일관성**: build 함수명·emit명·모듈 경로 표로 고정. data.mjs/ui-helpers.mjs export ↔ gen-slaydeck import 일치. +- **리스크**: 상수/헬퍼 참조 누락 → 바이트 diff 또는 throw로 즉시 노출. 증분 추출로 실패 범위 최소화. 단방향 의존(orchestrator→hud→lib)로 순환 없음.