From 064d81d424ef871983c9e3daedfdd6ec1830a92e Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:20:53 +0900 Subject: [PATCH 1/8] =?UTF-8?q?docs(spec):=20=EC=83=9D=EC=84=B1=EA=B8=B0?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=ED=99=94(Phase=201)=20+=20=ED=95=98?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=A6=AC=EB=93=9C=20UI=20=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=EB=A7=B5=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gen-slaydeck.mjs UI emit 16종을 lib/+hud/ 모듈로 분리(출력 바이트 동일·무위험). codeblock 메서드 제외. 하이브리드 단계적: Phase2 캐릭터선택 메이커 저작 파일럿. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...6-06-16-generator-modularization-design.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-16-generator-modularization-design.md diff --git a/docs/superpowers/specs/2026-06-16-generator-modularization-design.md b/docs/superpowers/specs/2026-06-16-generator-modularization-design.md new file mode 100644 index 0000000..fa1bf3f --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-generator-modularization-design.md @@ -0,0 +1,80 @@ +# 생성기 모듈화 (Phase 1) + 하이브리드 UI 로드맵 설계 + +작성일: 2026-06-16 +브랜치: `feature/gen-modularization` + +## 배경 / 동기 + +`DefaultGroup.ui`에 모든 UI(캐릭터 선택·상점·전체 덱·전투…)가 들어 있어, 사용자가 (a) **생성기 코드 유지보수**와 (b) **메이커에서 기능별 시각 편집**을 원함. + +핵심 제약(브레인스토밍에서 확정): MSW에서 "Layer"는 렌더 z순서일 뿐 논리 분리 도구가 아니고, 실제 UI 그룹 단위는 **UIGroup**(`.ui` 파일 = UIGroup, 현재 Default/Popup/Toast 3개). 그리고 **같은 UI를 '생성'과 '메이커 수동 편집' 둘 다로 둘 수 없음**(재생성이 수동 편집을 덮어씀). + +→ **소유 모델 = 하이브리드·단계적**(사용자 승인): 정적 레이아웃은 메이커 저작(시각 편집), 동적 내용은 컨트롤러가 런타임 주입. 단, 한 번에 안 하고 단계적으로. + +## 로드맵 (단계적 — 각 Phase가 자체 spec→plan) + +- **Phase 1 (이 문서)**: `gen-slaydeck.mjs`(~6,200줄)의 **UI emit을 기능별 모듈로 분리**. 출력 `.ui`/`codeblock` **바이트 동일**(순수 리팩터·무위험). (a) 충족 + (b) 토대(화면별 파일). +- **Phase 2 (후속 spec)**: 화면 1개(**캐릭터 선택**) 파일럿 — 정적 레이아웃을 메이커 저작 UIGroup으로 이관, 생성기는 그 화면 emit 중단, `SlayDeckController`가 경로로 내용(이미지·텍스트) 주입. (b) 패턴 검증. +- **Phase 3 (후속 spec)**: 검증되면 상점·전체덱 등으로 확장. + +## 현재 구조 (조사 결과) + +- **공유 인프라**(~48–530): `luaSoulShopTable`/`luaFramesTable`/`luaNodeIconsTable`/`luaRelicsTable`/`luaPotionsTable`/`luaIntentsArray`/`luaEnemiesTable`/`luaStr`/`luaJobsTable`/`luaCardsTable`/`luaDeckTable`/`frameRuid`/`cardFaceLayout`/`guid`/`transform`/`sprite`/`button`/`text`/`scrollLayoutGroup`/`entity`/`uiPath`/`sectionRoot`/`isGeneratedUiEntity`/`appendUiSection`. 데이터 로드 상수(CARDS/CHARS/ENEMIES/RELICS/POTIONS/CARDFRAMES/NODEICONS/CAM) 및 색·치수 상수(GOLD/WHITE/TRANSPARENT/ALIGN_*/CARD_W/CARD_H 등). +- **`guid(prefix, n)`은 순수 함수**(`:284`, 내부 카운터 없음; ns는 prefix→바이트 매핑). **모듈 호출 순서와 무관하게 동일 guid** → 분리해도 바이트 동일. +- **`upsertUi()`**(`:529`)가 UI 오케스트레이터: 기존 `DefaultGroup.ui` 로드 → 생성 섹션 필터(stock 보존) → 로컬 `emit(section, entities)` 클로저로 누적 → CardHand 스톡카드 in-place upsert(`:565–691`, 특수) → HUD별 `const x=[]; const add=…; add(entity(...)); …; emit('X', x)` → (말미) 병합·기록. +- **HUD emit 16종(순서·라인)**: DeckHud(`:808`) · DeckInspectHud(`:942`) · DeckAllHud(`:1097`) · CombatHud(`:1587`) · RewardHud(`:1681`) · MapHud(`:1839`) · ShopHud(`:2038`) · RestHud(`:2095`) · TreasureHud(`:2181`) · JobChoiceHud(`:2229`) · JobSelectHud(`:2314`) · MainMenu(`:2616`) · CharacterSelectHud(`:2617`) · LobbyHud(`:2672`) · BoardHud(`:2727`) · SoulShopHud(`:2814`). **각 섹션은 서로의 지역변수 비참조**(헬퍼·데이터 상수만 사용). +- **codeblock 메서드**(`prop`/`method`/`codeblock`/`writeCodeblocks` `:2836–6124`, ~3,200줄) + **patchCommon**(`:6125`). **Phase 1 범위 제외.** + +## Phase 1 상세 설계 + +### 목표 파일 구조 +``` +tools/deck/ + gen-slaydeck.mjs # 오케스트레이터(축소): import lib+hud → 데이터 로드 → upsertUi(HUD 모듈 순차) → writeCodeblocks → patchCommon + lib/ + ui-helpers.mjs # guid, transform, sprite, button, text, entity, scrollLayoutGroup, + # 상수(GOLD/WHITE/TRANSPARENT/ALIGN_*/CARD_W/CARD_H/UI_ROOT 등), cardFaceLayout, + # uiPath/sectionRoot/isGeneratedUiEntity/appendUiSection + data.mjs # CARDS/CHARS/ENEMIES/RELICS/POTIONS/CARDFRAMES/NODEICONS/CAM 로드·검증 + # + luaXxxTable·frameRuid + 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 +``` + +### 모듈 계약 +- 각 `hud/.mjs`: `export function build()` → 자기 HUD 엔티티 배열 반환. 필요한 헬퍼·상수·데이터는 `lib/`에서 **import**(거대 deps 객체 전달 금지). +- `upsertUi()`(오케스트레이터에 잔류)는 기존 **순서 그대로** `emit('DeckHud', buildDeckHud())` … `emit('SoulShopHud', buildSoulShop())` 호출. `emit`·섹션 병합 로직 불변. +- **CardHand 스톡카드 in-place upsert**(`:565–691`)는 기존 `.ui` 엔티티를 변형하는 특수 로직 → 오케스트레이터(또는 `hud/cardhand.mjs`)에 그대로 유지. import 경계만 정리. + +### 바이트 동일 불변식 (가장 중요) +- 리팩터는 **출력 변경 0**이 목표. 보장 근거: guid 순수·emit 순서·entity 구성 모두 보존, 로직 이동만. +- **합격 기준**: 리팩터 후 `node tools/deck/gen-slaydeck.mjs` → `git diff` 결과가 **`ui/DefaultGroup.ui`·`SlayDeckController.codeblock`에 0 변경**(`Global/common.gamelogic`은 LF churn만 허용 → `git checkout`). + +### 증분 실행 전략 +- 한 번에 16개 다 옮기지 말고 **HUD 1~2개씩 추출 → 재생성 → `git diff` 빈 결과 확인 → 커밋** 반복. 첫 추출(예: SoulShopHud 같은 말단 + lib 골격) 성공 후 패턴 반복. +- lib 추출(헬퍼·상수·데이터) 먼저 → 그 다음 HUD 모듈을 하나씩 lib import로 전환. + +### 미러/테스트·하네스 +- 전투규칙·맵생성 Lua **무변경** → `sim-balance`/`rogue-map` 미러 동기화 불필요(회귀 확인차 `node --test` 실행). +- **RULES 동기화**: 생성기가 다중 파일이 되므로 RULES §1 "단일 소스"/보조 생성기 표를 `tools/deck/`(gen-slaydeck + lib/ + hud/)로 갱신. + +## 범위 밖 (명시) +- codeblock 메서드(`method()` ~3,200줄) 분리 — 더 크고 (b)와 무관·리스크↑. 원하면 별도 **Phase 1b** spec. +- 게임 동작·데이터·런타임 로직 변경. (순수 소스 리팩터) +- UIGroup 분할·메이커 저작 이관 — Phase 2 이후. + +## 리스크 +- 클로저 참조(헬퍼/상수)를 import로 전환하는 광범위·기계적 수정 — 누락 시 런타임 throw 또는 출력 diff로 **즉시** 노출(바이트 검증이 안전망). +- 상수 정의 위치 산재(CARD_W·GOLD·UI_ROOT 등 top-level) — lib로 이동 시 누락 주의. 추출 전 `grep`으로 전체 상수 인벤토리 작성. +- ESM 순환 import 주의(lib는 hud를 import하지 않음 — 단방향: orchestrator→hud→lib). + +## 변경 파일 요약 +| 파일 | 변경 | +|---|---| +| `tools/deck/lib/ui-helpers.mjs`, `lib/data.mjs` | **신설** — 공유 헬퍼·상수·데이터 | +| `tools/deck/hud/*.mjs` (16) | **신설** — HUD별 build 함수 | +| `tools/deck/gen-slaydeck.mjs` | 오케스트레이터로 축소(데이터/UI emit 본문 → 모듈로 이동, import·호출만) | +| `RULES.md` | §1 보조 생성기/단일소스 표에 lib/·hud/ 반영 | +| `ui/DefaultGroup.ui`·`SlayDeckController.codeblock` | **무변경**(바이트 동일이 합격 기준) | -- 2.49.1 From 44878bab9e05c6c5be1041be6909dd6f2a1efa73 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:24:52 +0900 Subject: [PATCH 2/8] =?UTF-8?q?docs(plan):=20=EC=83=9D=EC=84=B1=EA=B8=B0?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=ED=99=94=20Phase=201=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EA=B3=84=ED=9A=8D=20(lib/+hud/,=20=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=8F=99=EC=9D=BC=20=EA=B2=8C=EC=9D=B4=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-16-generator-modularization.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-16-generator-modularization.md 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)로 순환 없음. -- 2.49.1 From fcc103227c9d196e1bafb588a870c8a675fd2b8c Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:30:28 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor(gen):=20lib/data.mjs=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=C2=B7lua=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=B6=94=EC=B6=9C=20(=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EB=8F=99=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gen-slaydeck.mjs의 데이터 로드·검증·luaXxxTable·게임상수(라인 3~188)를 tools/deck/lib/data.mjs로 이동, import로 연결. 산출물 무변경(diffcheck로 검증). + tools/verify/diffcheck.mjs: 워킹트리 vs HEAD 줄바꿈 정규화 비교(deny 회피) 게이트. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/deck/gen-slaydeck.mjs | 187 +---------------------------------- tools/deck/lib/data.mjs | 190 ++++++++++++++++++++++++++++++++++++ tools/verify/diffcheck.mjs | 20 ++++ 3 files changed, 211 insertions(+), 186 deletions(-) create mode 100644 tools/deck/lib/data.mjs create mode 100644 tools/verify/diffcheck.mjs diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index af88f68..c583277 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -1,191 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; -const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8')); -const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8')); - -// 검증 (fail-fast): 잘못된 데이터면 생성 중단 -const CLASSES = { - warrior: { label: '전사', maxHp: 80 }, - bandit: { label: '도적', maxHp: 70 }, - magician: { label: '마법사', maxHp: 70 }, -}; -for (const cls of Object.keys(CLASSES)) { - if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`); - for (const id of CARDS.starterDecks[cls]) { - if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`); - } -} -// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드) -const JOBS = { - warrior: [ - { id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' }, - { id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' }, - { id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' }, - ], - magician: [ - { id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' }, - { id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' }, - { id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' }, - ], - bandit: [ - { id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' }, - { id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' }, - { id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' }, - ], -}; -for (const [cls, jobs] of Object.entries(JOBS)) { - for (const j of jobs) { - if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`); - } -} -// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점 -const SOUL_UNLOCKS = [ - { key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 }, - { key: 'hp', name: '단련된 육체', desc: '시작 최대 HP +15', cost: 4 }, - { key: 'trim', name: '덱 정제', desc: '시작 덱에서 기본 카드 1장 제거', cost: 5 }, - { key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 }, -]; -function luaSoulShopTable(unlocks) { - const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n'); - return `self.SoulShopDef = {\n${items}\n}`; -} -if (!ENEMIES.enemies[ENEMIES.activeEnemy]) { - throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`); -} - -// 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종) -const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8')); -const RARITIES = ['normal', 'unique', 'legend']; -for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) { - for (const r of RARITIES) { - if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`); - } -} -for (const [id, c] of Object.entries(CARDS.cards)) { - if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`); - const fc = CARDFRAMES.classToFrame[c.class]; - if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`); -} -function frameRuid(card) { - return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity]; -} -function luaFramesTable() { - const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) => - `\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n'); - const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n'); - return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`; -} -function luaNodeIconsTable() { - const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n'); - return `self.NodeIcons = {\n${rows}\n}`; -} - -// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨. -const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7) -const MAP_COLS = 4; - -// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별) -const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71'; -const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759'; - -// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성) -const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8')); -for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) { - if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`); -} -if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류'); - -// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성) -const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8')); -for (const c of ['warrior', 'magician', 'bandit']) { - if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`); -} - -// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용. -const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8')); - -const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8')); -if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`); -for (const id of RELICS.relicPool) { - if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`); -} -function luaRelicsTable(relics) { - const lines = Object.entries(relics).map(([id, r]) => - `\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`); - return `self.Relics = {\n${lines.join('\n')}\n}`; -} - -const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8')); -for (const [pid, p] of Object.entries(POTIONS.potions)) { - if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`); -} -function luaPotionsTable(potions) { - const lines = Object.entries(potions).map(([id, p]) => - `\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`); - return `self.Potions = {\n${lines.join('\n')}\n}`; -} - -function luaIntentsArray(intents) { - return '{ ' + intents.map((it) => { - const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value != null ? it.value : 0}`]; - if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`); - if (it.card != null) fields.push(`card = ${luaStr(it.card)}`); - if (it.count != null) fields.push(`count = ${it.count}`); - return `{ ${fields.join(', ')} }`; - }).join(', ') + ' }'; -} -function luaEnemiesTable(enemies) { - const lines = Object.entries(enemies).map(([id, e]) => - `\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`); - return `self.Enemies = {\n${lines.join('\n')}\n}`; -} -// Lua 직렬화 헬퍼 -function luaStr(s) { - return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"'; -} -function luaJobsTable(jobs) { - const cls = Object.entries(jobs).map(([clsId, list]) => { - const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n'); - return `\t${clsId} = {\n${items}\n\t},`; - }).join('\n'); - return `self.Jobs = {\n${cls}\n}`; -} -function luaCardsTable(cards) { - const lines = Object.entries(cards).map(([id, c]) => { - const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`]; - if (c.damage != null) fields.push(`damage = ${c.damage}`); - if (c.block != null) fields.push(`block = ${c.block}`); - 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}`); - if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`); - fields.push(`class = ${luaStr(c.class)}`); - fields.push(`rarity = ${luaStr(c.rarity)}`); - if (c.hits != null) fields.push(`hits = ${c.hits}`); - if (c.pierce === true) fields.push('pierce = true'); - if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`); - if (c.draw != null) fields.push(`draw = ${c.draw}`); - if (c.heal != null) fields.push(`heal = ${c.heal}`); - if (c.poison != null) fields.push(`poison = ${c.poison}`); - if (c.discard != null) fields.push(`discard = ${c.discard}`); - if (c.discardAll === true) fields.push('discardAll = true'); - if (c.sly === true) fields.push('sly = true'); - if (c.retain === true) fields.push('retain = true'); - if (c.aoe === true) fields.push('aoe = true'); - if (c.unplayable === true) fields.push('unplayable = true'); - if (c.curse === true) fields.push('curse = true'); - if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${c.endTurnDamage}`); - if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`); - if (c.image != null) fields.push(`image = ${luaStr(c.image)}`); - return `\t${id} = { ${fields.join(', ')} },`; - }); - return `self.Cards = {\n${lines.join('\n')}\n}`; -} -function luaDeckTable(deck) { - return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`; -} +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs'; const UI_FILE = 'ui/DefaultGroup.ui'; const COMMON_FILE = 'Global/common.gamelogic'; diff --git a/tools/deck/lib/data.mjs b/tools/deck/lib/data.mjs new file mode 100644 index 0000000..93d2c67 --- /dev/null +++ b/tools/deck/lib/data.mjs @@ -0,0 +1,190 @@ +import { readFileSync } from 'node:fs'; + +const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8')); +const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8')); + +// 검증 (fail-fast): 잘못된 데이터면 생성 중단 +const CLASSES = { + warrior: { label: '전사', maxHp: 80 }, + bandit: { label: '도적', maxHp: 70 }, + magician: { label: '마법사', maxHp: 70 }, +}; +for (const cls of Object.keys(CLASSES)) { + if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`); + for (const id of CARDS.starterDecks[cls]) { + if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`); + } +} +// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드) +const JOBS = { + warrior: [ + { id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' }, + { id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' }, + { id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' }, + ], + magician: [ + { id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' }, + { id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' }, + { id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' }, + ], + bandit: [ + { id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' }, + { id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' }, + { id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' }, + ], +}; +for (const [cls, jobs] of Object.entries(JOBS)) { + for (const j of jobs) { + if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`); + } +} +// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점 +const SOUL_UNLOCKS = [ + { key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 }, + { key: 'hp', name: '단련된 육체', desc: '시작 최대 HP +15', cost: 4 }, + { key: 'trim', name: '덱 정제', desc: '시작 덱에서 기본 카드 1장 제거', cost: 5 }, + { key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 }, +]; +function luaSoulShopTable(unlocks) { + const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n'); + return `self.SoulShopDef = {\n${items}\n}`; +} +if (!ENEMIES.enemies[ENEMIES.activeEnemy]) { + throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`); +} + +// 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종) +const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8')); +const RARITIES = ['normal', 'unique', 'legend']; +for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) { + for (const r of RARITIES) { + if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`); + } +} +for (const [id, c] of Object.entries(CARDS.cards)) { + if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`); + const fc = CARDFRAMES.classToFrame[c.class]; + if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`); +} +function frameRuid(card) { + return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity]; +} +function luaFramesTable() { + const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) => + `\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n'); + const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n'); + return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`; +} +function luaNodeIconsTable() { + const rows = Object.entries(NODEICONS.icons).map(([t, ruid]) => `\t${t} = ${luaStr(ruid)},`).join('\n'); + return `self.NodeIcons = {\n${rows}\n}`; +} + +// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨. +const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7) +const MAP_COLS = 4; + +// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별) +const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71'; +const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759'; + +// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성) +const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8')); +for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) { + if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`); +} +if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류'); + +// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성) +const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8')); +for (const c of ['warrior', 'magician', 'bandit']) { + if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`); +} + +// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용. +const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8')); + +const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8')); +if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`); +for (const id of RELICS.relicPool) { + if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`); +} +function luaRelicsTable(relics) { + const lines = Object.entries(relics).map(([id, r]) => + `\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`); + return `self.Relics = {\n${lines.join('\n')}\n}`; +} + +const POTIONS = JSON.parse(readFileSync('data/potions.json', 'utf8')); +for (const [pid, p] of Object.entries(POTIONS.potions)) { + if (!p.name || !p.effect || p.value == null) throw new Error(`[gen-slaydeck] potion 필드 누락: ${pid}`); +} +function luaPotionsTable(potions) { + const lines = Object.entries(potions).map(([id, p]) => + `\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`); + return `self.Potions = {\n${lines.join('\n')}\n}`; +} + +function luaIntentsArray(intents) { + return '{ ' + intents.map((it) => { + const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value != null ? it.value : 0}`]; + if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`); + if (it.card != null) fields.push(`card = ${luaStr(it.card)}`); + if (it.count != null) fields.push(`count = ${it.count}`); + return `{ ${fields.join(', ')} }`; + }).join(', ') + ' }'; +} +function luaEnemiesTable(enemies) { + const lines = Object.entries(enemies).map(([id, e]) => + `\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`); + return `self.Enemies = {\n${lines.join('\n')}\n}`; +} +// Lua 직렬화 헬퍼 +function luaStr(s) { + return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"'; +} +function luaJobsTable(jobs) { + const cls = Object.entries(jobs).map(([clsId, list]) => { + const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n'); + return `\t${clsId} = {\n${items}\n\t},`; + }).join('\n'); + return `self.Jobs = {\n${cls}\n}`; +} +function luaCardsTable(cards) { + const lines = Object.entries(cards).map(([id, c]) => { + const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`]; + if (c.damage != null) fields.push(`damage = ${c.damage}`); + if (c.block != null) fields.push(`block = ${c.block}`); + 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}`); + if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`); + fields.push(`class = ${luaStr(c.class)}`); + fields.push(`rarity = ${luaStr(c.rarity)}`); + if (c.hits != null) fields.push(`hits = ${c.hits}`); + if (c.pierce === true) fields.push('pierce = true'); + if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`); + if (c.draw != null) fields.push(`draw = ${c.draw}`); + if (c.heal != null) fields.push(`heal = ${c.heal}`); + if (c.poison != null) fields.push(`poison = ${c.poison}`); + if (c.discard != null) fields.push(`discard = ${c.discard}`); + if (c.discardAll === true) fields.push('discardAll = true'); + if (c.sly === true) fields.push('sly = true'); + if (c.retain === true) fields.push('retain = true'); + if (c.aoe === true) fields.push('aoe = true'); + if (c.unplayable === true) fields.push('unplayable = true'); + if (c.curse === true) fields.push('curse = true'); + if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${c.endTurnDamage}`); + if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`); + if (c.image != null) fields.push(`image = ${luaStr(c.image)}`); + return `\t${id} = { ${fields.join(', ')} },`; + }); + return `self.Cards = {\n${lines.join('\n')}\n}`; +} +function luaDeckTable(deck) { + return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`; +} + +export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable }; diff --git a/tools/verify/diffcheck.mjs b/tools/verify/diffcheck.mjs new file mode 100644 index 0000000..25889c7 --- /dev/null +++ b/tools/verify/diffcheck.mjs @@ -0,0 +1,20 @@ +import { readFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; + +// 산출물 바이트-동일 게이트: 워킹트리 vs HEAD(blob)를 줄바꿈 정규화 후 비교. +// 산출물 경로를 Bash 명령줄에 노출하지 않아 settings.json deny를 회피(count.mjs와 동일 취지). +// 사용: node tools/verify/diffcheck.mjs +const FILES = [ + 'ui/DefaultGroup.ui', + 'RootDesk/MyDesk/SlayDeckController.codeblock', + 'Global/common.gamelogic', +]; +let allSame = true; +for (const f of FILES) { + const work = readFileSync(f, 'utf8').replace(/\r\n/g, '\n'); + const blob = execSync(`git show HEAD:${f}`, { encoding: 'utf8', maxBuffer: 1 << 30 }).replace(/\r\n/g, '\n'); + const same = work === blob; + if (!same) allSame = false; + console.log(`${same ? 'IDENTICAL ' : 'DIFFER '} ${f}${same ? '' : ` (work=${work.length} blob=${blob.length})`}`); +} +console.log(allSame ? '\n=> 산출물 바이트-동일 (리팩터 안전)' : '\n=> 차이 있음 (내용 변경 — 확인 필요)'); -- 2.49.1 From e6a397cc559f026ab4fdcb9eb1e033784ed90825 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:31:59 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor(gen):=20lib/ui-helpers.mjs?= =?UTF-8?q?=EB=A1=9C=20UI=20=ED=97=AC=ED=8D=BC=C2=B7=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20(=EC=B6=9C=EB=A0=A5=20=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=8F=99=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI_FILE~appendUiSection(상수 30 + 헬퍼 15, 총 45)을 tools/deck/lib/ui-helpers.mjs로 이동, import로 연결. 산출물 무변경(diffcheck IDENTICAL). Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/deck/gen-slaydeck.mjs | 340 +-------------------------------- tools/deck/lib/ui-helpers.mjs | 341 ++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+), 339 deletions(-) create mode 100644 tools/deck/lib/ui-helpers.mjs diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index c583277..8282454 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -2,345 +2,7 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs'; -const UI_FILE = 'ui/DefaultGroup.ui'; -const COMMON_FILE = 'Global/common.gamelogic'; -const UI_ROOT = '/ui/DefaultGroup'; -const GENERATED_UI_SECTIONS = [ - 'DeckHud', - 'DeckInspectHud', - 'DeckAllHud', - 'CombatHud', - 'RewardHud', - 'MapHud', - 'ShopHud', - 'RestHud', - 'TreasureHud', - 'JobChoiceHud', - 'JobSelectHud', - 'MainMenu', - 'CharacterSelectHud', - 'LobbyHud', - 'BoardHud', - 'SoulShopHud', -]; -const UI_APPEND_ORDER = [ - 'DeckHud', - 'CombatHud', - 'RewardHud', - 'MapHud', - 'ShopHud', - 'RestHud', - 'TreasureHud', - 'JobChoiceHud', - 'JobSelectHud', - 'DeckInspectHud', - 'DeckAllHud', - 'MainMenu', - 'CharacterSelectHud', - 'LobbyHud', - 'BoardHud', - 'SoulShopHud', -]; -const DISABLED_STOCK_CONTROLS = ['Button_Attack', 'Button_Jump', 'UIJoystick']; - -const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 }; -const DARK = { r: 0.08, g: 0.09, b: 0.11, a: 0.92 }; -const GOLD = { r: 0.94, g: 0.74, b: 0.26, a: 1 }; -const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1 }; -const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1 }; -const SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 }; -const DAMAGE_DIGIT_RUIDS = [ - 'b94c19830538447f81617035d89bcc05', - '01b023122a6f4a5789e1d4c61ff8f430', - '57ff71d1b9eb471b9feb1c15348770c9', - 'cab92837798a42ad9143c67e93f999e1', - '366f271f9ca94a0684083aad9298efad', - '5c7a6ad38491466aa84bf450e0fdcf25', - '7d82a6838e1b4f4a8a0f7420db34c985', - 'c0765bb1e47d46ffbe1df4ac19ea9b1b', - '6ea0bfed61e149f88a9b3f22dd79774f', - '82ad2acaae4e4b3fb87bf73635250d22', -]; -const DAMAGE_POP_MAX_DIGITS = 5; -const DAMAGE_POP_DIGIT_W = 22; -const DAMAGE_POP_DIGIT_H = 32; -const DAMAGE_POP_DIGIT_SPACING = -4; - -const MAX_MONSTERS = 4; -const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산 - -const HP_BAR_W = 140; -const WHITE = { r: 1, g: 1, b: 1, a: 1 }; -const CARD_NAME_TEXT = { r: 1, g: 0.92, b: 0.62, a: 1 }; -const CARD_DESC_TEXT = { r: 0.98, g: 0.96, b: 0.9, a: 1 }; -// 카드 프레임(1054×1492 원본) 슬롯 레이아웃 — 픽셀 실측을 180×250 카드 좌표로 환산한 기준값을 폭 비례 스케일. -// 실측(워리어·메이지·밴딧 공통): 육각 중심 (120,127)→(-70,104) · 배너 본체 y55..165, x215..1015→중심 (+15,+107) -// · 설명 박스 y~1030..1480→중심 (0,-86) · 아트 영역 y260..1030→중심 (0,+17) -function cardFaceLayout(W) { - const s = W / 180; - const r = (v) => Math.round(v * s); - return { - texts: [ - ['Cost', { size: { x: r(40), y: r(40) }, pos: { x: r(-70), y: r(104) }, fontSize: r(24), bold: true, color: WHITE, dropShadow: false, outlineWidth: 2 }], - ['Name', { size: { x: r(142), y: r(28) }, pos: { x: r(15), y: r(106) }, fontSize: r(17), bold: true, color: CARD_NAME_TEXT, dropShadow: false, outlineWidth: 2 }], - ['Desc', { size: { x: r(158), y: r(78) }, pos: { x: 0, y: r(-82) }, fontSize: r(14), bold: true, color: CARD_DESC_TEXT, dropShadow: false, outlineWidth: 2 }], - ], - art: { size: { x: r(112), y: r(112) }, pos: { x: 0, y: r(17) } }, - }; -} -const CARD_W = 180; -const CARD_H = 250; -const CARD_SPACING = 200; -const CARD_XS = [-400, -200, 0, 200, 400]; - -const ALIGN_CENTER = 0; -const ALIGN_BOTTOM_CENTER = 6; - -function guid(prefix, n) { - // 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑. - const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4 - : prefix === 'ins2' ? 0xe5 : prefix === 'all2' ? 0xe6 : prefix === 'rwd2' ? 0xe7 : prefix === 'shp2' ? 0xe8 : prefix === 'lob' ? 0xe9 : prefix === 'brd' ? 0xea : prefix === 'soul' ? 0xeb : 0xfe; - const v = (ns * 0x100000 + n) >>> 0; - return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`; -} - -function transform({ parentW, parentH, anchor, pivot, size, pos, align = 0 }) { - const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y }; - const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y }; - return { - '@type': 'MOD.Core.UITransformComponent', - ActivePlatform: 255, - AlignmentOption: align, - AnchorsMax: anchor, - AnchorsMin: anchor, - MobileOnly: false, - OffsetMax: offMax, - OffsetMin: offMin, - Pivot: pivot, - RectSize: size, - UIMode: 1, - UIScale: { x: 1, y: 1, z: 1 }, - UIVersion: 2, - anchoredPosition: pos, - Position: { x: anchor.x * parentW - parentW / 2 + pos.x, y: anchor.y * parentH - parentH / 2 + pos.y, z: 0 }, - QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 }, - Scale: { x: 1, y: 1, z: 1 }, - Enable: true, - }; -} - -function sprite({ dataId = '', color = TRANSPARENT, type = 1, raycast = false }) { - return { - '@type': 'MOD.Core.SpriteGUIRendererComponent', - AnimClipPlayType: 0, - EndFrameIndex: 2147483647, - ImageRUID: { DataId: dataId }, - LocalPosition: { x: 0, y: 0 }, - LocalScale: { x: 1, y: 1 }, - OverrideSorting: false, - PlayRate: 1, - PreserveSprite: 0, - StartFrameIndex: 0, - Color: color, - DropShadow: false, - DropShadowAngle: 30, - DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, - DropShadowDistance: 32, - FillAmount: 1, - FillCenter: true, - FillClockWise: true, - FillMethod: 0, - FillOrigin: 0, - FlipX: false, - FlipY: false, - FrameColumn: 1, - FrameRate: 0, - FrameRow: 1, - Outline: false, - OutlineColor: { r: 0, g: 0, b: 0, a: 1 }, - OutlineWidth: 3, - RaycastTarget: raycast, - Type: type, - Enable: true, - }; -} - -function button({ enabled = true } = {}) { - return { - '@type': 'MOD.Core.ButtonComponent', - Colors: { - NormalColor: { r: 1, g: 1, b: 1, a: 1 }, - HighlightedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, - PressedColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 1 }, - SelectedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, - DisabledColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 0.5019608 }, - ColorMultiplier: 1, - FadeDuration: 0.1, - }, - ImageRUIDs: { - HighlightedSprite: null, - PressedSprite: null, - SelectedSprite: null, - DisabledSprite: null, - }, - KeyCode: 0, - OverrideSorting: false, - Transition: 1, - Enable: enabled, - }; -} - -function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4, dropShadow = false, outlineWidth = 1 }) { - return { - '@type': 'MOD.Core.TextComponent', - Alignment: alignment, - Bold: bold, - DropShadow: dropShadow, - DropShadowAngle: 30, - DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, - DropShadowDistance: dropShadow ? 18 : 32, - Font: 0, - FontColor: color, - FontSize: fontSize, - MaxSize: fontSize, - MinSize: 8, - OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 }, - OutlineDistance: { x: 1, y: -1 }, - OutlineWidth: outlineWidth, - Overflow: 0, - OverrideSorting: false, - Padding: { left: 0, right: 0, top: 0, bottom: 0 }, - SizeFit: false, - Text: value, - UseOutLine: true, - Enable: true, - }; -} - -function scrollLayoutGroup({ cellSize, spacing, columns }) { - return { - '@type': 'MOD.Core.ScrollLayoutGroupComponent', - CellSize: cellSize, - ChildAlignment: 0, - Constraint: 1, - ConstraintCount: columns, - GridChildAlignment: 0, - GridSpacing: spacing, - HorizontalScrollBarDirection: 0, - IgnoreMapLayerCheck: false, - OrderInLayer: 0, - OverrideSorting: false, - Padding: { left: 16, right: 16, top: 16, bottom: 16 }, - ReverseArrangement: false, - ScrollBarBackgroundColor: { r: 1, g: 1, b: 1, a: 0.18 }, - ScrollBarBgImageRUID: { DataId: '' }, - ScrollBarHandleColor: { r: 0.94, g: 0.74, b: 0.26, a: 0.9 }, - ScrollBarHandleImageRUID: { DataId: '' }, - ScrollBarThickness: 12, - ScrollBarVisible: 1, - SortingLayer: 'UI', - Spacing: 0, - StartAxis: 0, - StartCorner: 0, - Type: 2, - UseScroll: true, - VerticalScrollBarDirection: 1, - Enable: true, - }; -} - -function popupLayerFor(path) { - if (path.startsWith('/ui/DefaultGroup/DeckAllHud')) return { root: '/ui/DefaultGroup/DeckAllHud', base: 4000 }; - if (path.startsWith('/ui/DefaultGroup/DeckInspectHud')) return { root: '/ui/DefaultGroup/DeckInspectHud', base: 3000 }; - return null; -} - -function uiOrderFor(path, displayOrder) { - const popup = popupLayerFor(path); - if (popup != null) { - const relative = path.slice(popup.root.length).split('/').filter(Boolean); - return popup.base + relative.length * 100 + displayOrder; - } - return displayOrder; -} - -function displayOrderFor(path, displayOrder) { - return uiOrderFor(path, displayOrder); -} - -function applySortingOverride(path, components, displayOrder) { - if (popupLayerFor(path) == null) return components; - const order = uiOrderFor(path, displayOrder); - return components.map((component) => { - if (component['@type'] !== 'MOD.Core.SpriteGUIRendererComponent' && component['@type'] !== 'MOD.Core.TextComponent') { - return component; - } - return { - ...component, - OverrideSorting: true, - SortingLayer: 'UI', - OrderInLayer: order, - }; - }); -} - -function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) { - const parts = path.split('/'); - const name = parts[parts.length - 1]; - const sortedComponents = applySortingOverride(path, components, displayOrder); - return { - id, - path, - componentNames, - jsonString: { - name, - path, - nameEditable: true, - enable: true, - visible: true, - localize: true, - displayOrder: displayOrderFor(path, displayOrder), - pathConstraints: '/'.repeat(parts.length - 1), - revision: 1, - origin: { - type: 'Model', - entry_id: entryId, - sub_entity_id: null, - root_entity_id: null, - replaced_model_id: null, - }, - modelId, - '@components': sortedComponents, - '@version': 1, - }, - }; -} - -function uiPath(...parts) { - return [UI_ROOT, ...parts].join('/'); -} - -function sectionRoot(section) { - return uiPath(section); -} - -function isGeneratedUiEntity(e) { - return GENERATED_UI_SECTIONS.some((section) => e.path.startsWith(sectionRoot(section))); -} - -function appendUiSection(ui, section, entities) { - if (!GENERATED_UI_SECTIONS.includes(section)) { - throw new Error(`[gen-slaydeck] unknown generated UI section: ${section}`); - } - const root = sectionRoot(section); - for (const e of entities) { - if (!e.path.startsWith(root)) { - throw new Error(`[gen-slaydeck] ${section} section emitted unexpected path: ${e.path}`); - } - } - ui.ContentProto.Entities.push(...entities); -} - +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from './lib/ui-helpers.mjs'; function upsertUi() { const ui = JSON.parse(readFileSync(UI_FILE, 'utf8')); const E = ui.ContentProto.Entities; diff --git a/tools/deck/lib/ui-helpers.mjs b/tools/deck/lib/ui-helpers.mjs new file mode 100644 index 0000000..27b7381 --- /dev/null +++ b/tools/deck/lib/ui-helpers.mjs @@ -0,0 +1,341 @@ +const UI_FILE = 'ui/DefaultGroup.ui'; +const COMMON_FILE = 'Global/common.gamelogic'; +const UI_ROOT = '/ui/DefaultGroup'; +const GENERATED_UI_SECTIONS = [ + 'DeckHud', + 'DeckInspectHud', + 'DeckAllHud', + 'CombatHud', + 'RewardHud', + 'MapHud', + 'ShopHud', + 'RestHud', + 'TreasureHud', + 'JobChoiceHud', + 'JobSelectHud', + 'MainMenu', + 'CharacterSelectHud', + 'LobbyHud', + 'BoardHud', + 'SoulShopHud', +]; +const UI_APPEND_ORDER = [ + 'DeckHud', + 'CombatHud', + 'RewardHud', + 'MapHud', + 'ShopHud', + 'RestHud', + 'TreasureHud', + 'JobChoiceHud', + 'JobSelectHud', + 'DeckInspectHud', + 'DeckAllHud', + 'MainMenu', + 'CharacterSelectHud', + 'LobbyHud', + 'BoardHud', + 'SoulShopHud', +]; +const DISABLED_STOCK_CONTROLS = ['Button_Attack', 'Button_Jump', 'UIJoystick']; + +const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 }; +const DARK = { r: 0.08, g: 0.09, b: 0.11, a: 0.92 }; +const GOLD = { r: 0.94, g: 0.74, b: 0.26, a: 1 }; +const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1 }; +const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1 }; +const SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 }; +const DAMAGE_DIGIT_RUIDS = [ + 'b94c19830538447f81617035d89bcc05', + '01b023122a6f4a5789e1d4c61ff8f430', + '57ff71d1b9eb471b9feb1c15348770c9', + 'cab92837798a42ad9143c67e93f999e1', + '366f271f9ca94a0684083aad9298efad', + '5c7a6ad38491466aa84bf450e0fdcf25', + '7d82a6838e1b4f4a8a0f7420db34c985', + 'c0765bb1e47d46ffbe1df4ac19ea9b1b', + '6ea0bfed61e149f88a9b3f22dd79774f', + '82ad2acaae4e4b3fb87bf73635250d22', +]; +const DAMAGE_POP_MAX_DIGITS = 5; +const DAMAGE_POP_DIGIT_W = 22; +const DAMAGE_POP_DIGIT_H = 32; +const DAMAGE_POP_DIGIT_SPACING = -4; + +const MAX_MONSTERS = 4; +const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산 + +const HP_BAR_W = 140; +const WHITE = { r: 1, g: 1, b: 1, a: 1 }; +const CARD_NAME_TEXT = { r: 1, g: 0.92, b: 0.62, a: 1 }; +const CARD_DESC_TEXT = { r: 0.98, g: 0.96, b: 0.9, a: 1 }; +// 카드 프레임(1054×1492 원본) 슬롯 레이아웃 — 픽셀 실측을 180×250 카드 좌표로 환산한 기준값을 폭 비례 스케일. +// 실측(워리어·메이지·밴딧 공통): 육각 중심 (120,127)→(-70,104) · 배너 본체 y55..165, x215..1015→중심 (+15,+107) +// · 설명 박스 y~1030..1480→중심 (0,-86) · 아트 영역 y260..1030→중심 (0,+17) +function cardFaceLayout(W) { + const s = W / 180; + const r = (v) => Math.round(v * s); + return { + texts: [ + ['Cost', { size: { x: r(40), y: r(40) }, pos: { x: r(-70), y: r(104) }, fontSize: r(24), bold: true, color: WHITE, dropShadow: false, outlineWidth: 2 }], + ['Name', { size: { x: r(142), y: r(28) }, pos: { x: r(15), y: r(106) }, fontSize: r(17), bold: true, color: CARD_NAME_TEXT, dropShadow: false, outlineWidth: 2 }], + ['Desc', { size: { x: r(158), y: r(78) }, pos: { x: 0, y: r(-82) }, fontSize: r(14), bold: true, color: CARD_DESC_TEXT, dropShadow: false, outlineWidth: 2 }], + ], + art: { size: { x: r(112), y: r(112) }, pos: { x: 0, y: r(17) } }, + }; +} +const CARD_W = 180; +const CARD_H = 250; +const CARD_SPACING = 200; +const CARD_XS = [-400, -200, 0, 200, 400]; + +const ALIGN_CENTER = 0; +const ALIGN_BOTTOM_CENTER = 6; + +function guid(prefix, n) { + // 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑. + const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4 + : prefix === 'ins2' ? 0xe5 : prefix === 'all2' ? 0xe6 : prefix === 'rwd2' ? 0xe7 : prefix === 'shp2' ? 0xe8 : prefix === 'lob' ? 0xe9 : prefix === 'brd' ? 0xea : prefix === 'soul' ? 0xeb : 0xfe; + const v = (ns * 0x100000 + n) >>> 0; + return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`; +} + +function transform({ parentW, parentH, anchor, pivot, size, pos, align = 0 }) { + const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y }; + const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y }; + return { + '@type': 'MOD.Core.UITransformComponent', + ActivePlatform: 255, + AlignmentOption: align, + AnchorsMax: anchor, + AnchorsMin: anchor, + MobileOnly: false, + OffsetMax: offMax, + OffsetMin: offMin, + Pivot: pivot, + RectSize: size, + UIMode: 1, + UIScale: { x: 1, y: 1, z: 1 }, + UIVersion: 2, + anchoredPosition: pos, + Position: { x: anchor.x * parentW - parentW / 2 + pos.x, y: anchor.y * parentH - parentH / 2 + pos.y, z: 0 }, + QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 }, + Scale: { x: 1, y: 1, z: 1 }, + Enable: true, + }; +} + +function sprite({ dataId = '', color = TRANSPARENT, type = 1, raycast = false }) { + return { + '@type': 'MOD.Core.SpriteGUIRendererComponent', + AnimClipPlayType: 0, + EndFrameIndex: 2147483647, + ImageRUID: { DataId: dataId }, + LocalPosition: { x: 0, y: 0 }, + LocalScale: { x: 1, y: 1 }, + OverrideSorting: false, + PlayRate: 1, + PreserveSprite: 0, + StartFrameIndex: 0, + Color: color, + DropShadow: false, + DropShadowAngle: 30, + DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, + DropShadowDistance: 32, + FillAmount: 1, + FillCenter: true, + FillClockWise: true, + FillMethod: 0, + FillOrigin: 0, + FlipX: false, + FlipY: false, + FrameColumn: 1, + FrameRate: 0, + FrameRow: 1, + Outline: false, + OutlineColor: { r: 0, g: 0, b: 0, a: 1 }, + OutlineWidth: 3, + RaycastTarget: raycast, + Type: type, + Enable: true, + }; +} + +function button({ enabled = true } = {}) { + return { + '@type': 'MOD.Core.ButtonComponent', + Colors: { + NormalColor: { r: 1, g: 1, b: 1, a: 1 }, + HighlightedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, + PressedColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 1 }, + SelectedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, + DisabledColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 0.5019608 }, + ColorMultiplier: 1, + FadeDuration: 0.1, + }, + ImageRUIDs: { + HighlightedSprite: null, + PressedSprite: null, + SelectedSprite: null, + DisabledSprite: null, + }, + KeyCode: 0, + OverrideSorting: false, + Transition: 1, + Enable: enabled, + }; +} + +function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4, dropShadow = false, outlineWidth = 1 }) { + return { + '@type': 'MOD.Core.TextComponent', + Alignment: alignment, + Bold: bold, + DropShadow: dropShadow, + DropShadowAngle: 30, + DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, + DropShadowDistance: dropShadow ? 18 : 32, + Font: 0, + FontColor: color, + FontSize: fontSize, + MaxSize: fontSize, + MinSize: 8, + OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 }, + OutlineDistance: { x: 1, y: -1 }, + OutlineWidth: outlineWidth, + Overflow: 0, + OverrideSorting: false, + Padding: { left: 0, right: 0, top: 0, bottom: 0 }, + SizeFit: false, + Text: value, + UseOutLine: true, + Enable: true, + }; +} + +function scrollLayoutGroup({ cellSize, spacing, columns }) { + return { + '@type': 'MOD.Core.ScrollLayoutGroupComponent', + CellSize: cellSize, + ChildAlignment: 0, + Constraint: 1, + ConstraintCount: columns, + GridChildAlignment: 0, + GridSpacing: spacing, + HorizontalScrollBarDirection: 0, + IgnoreMapLayerCheck: false, + OrderInLayer: 0, + OverrideSorting: false, + Padding: { left: 16, right: 16, top: 16, bottom: 16 }, + ReverseArrangement: false, + ScrollBarBackgroundColor: { r: 1, g: 1, b: 1, a: 0.18 }, + ScrollBarBgImageRUID: { DataId: '' }, + ScrollBarHandleColor: { r: 0.94, g: 0.74, b: 0.26, a: 0.9 }, + ScrollBarHandleImageRUID: { DataId: '' }, + ScrollBarThickness: 12, + ScrollBarVisible: 1, + SortingLayer: 'UI', + Spacing: 0, + StartAxis: 0, + StartCorner: 0, + Type: 2, + UseScroll: true, + VerticalScrollBarDirection: 1, + Enable: true, + }; +} + +function popupLayerFor(path) { + if (path.startsWith('/ui/DefaultGroup/DeckAllHud')) return { root: '/ui/DefaultGroup/DeckAllHud', base: 4000 }; + if (path.startsWith('/ui/DefaultGroup/DeckInspectHud')) return { root: '/ui/DefaultGroup/DeckInspectHud', base: 3000 }; + return null; +} + +function uiOrderFor(path, displayOrder) { + const popup = popupLayerFor(path); + if (popup != null) { + const relative = path.slice(popup.root.length).split('/').filter(Boolean); + return popup.base + relative.length * 100 + displayOrder; + } + return displayOrder; +} + +function displayOrderFor(path, displayOrder) { + return uiOrderFor(path, displayOrder); +} + +function applySortingOverride(path, components, displayOrder) { + if (popupLayerFor(path) == null) return components; + const order = uiOrderFor(path, displayOrder); + return components.map((component) => { + if (component['@type'] !== 'MOD.Core.SpriteGUIRendererComponent' && component['@type'] !== 'MOD.Core.TextComponent') { + return component; + } + return { + ...component, + OverrideSorting: true, + SortingLayer: 'UI', + OrderInLayer: order, + }; + }); +} + +function entity({ id, path, modelId, entryId, componentNames, components, displayOrder }) { + const parts = path.split('/'); + const name = parts[parts.length - 1]; + const sortedComponents = applySortingOverride(path, components, displayOrder); + return { + id, + path, + componentNames, + jsonString: { + name, + path, + nameEditable: true, + enable: true, + visible: true, + localize: true, + displayOrder: displayOrderFor(path, displayOrder), + pathConstraints: '/'.repeat(parts.length - 1), + revision: 1, + origin: { + type: 'Model', + entry_id: entryId, + sub_entity_id: null, + root_entity_id: null, + replaced_model_id: null, + }, + modelId, + '@components': sortedComponents, + '@version': 1, + }, + }; +} + +function uiPath(...parts) { + return [UI_ROOT, ...parts].join('/'); +} + +function sectionRoot(section) { + return uiPath(section); +} + +function isGeneratedUiEntity(e) { + return GENERATED_UI_SECTIONS.some((section) => e.path.startsWith(sectionRoot(section))); +} + +function appendUiSection(ui, section, entities) { + if (!GENERATED_UI_SECTIONS.includes(section)) { + throw new Error(`[gen-slaydeck] unknown generated UI section: ${section}`); + } + const root = sectionRoot(section); + for (const e of entities) { + if (!e.path.startsWith(root)) { + throw new Error(`[gen-slaydeck] ${section} section emitted unexpected path: ${e.path}`); + } + } + ui.ContentProto.Entities.push(...entities); +} + + +export { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection }; -- 2.49.1 From bc266b188589feac5cab790cd1d9a723f160ad27 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:39:20 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor(gen):=20HUD=2014=EC=A2=85=EC=9D=84?= =?UTF-8?q?=20hud/*.mjs=EB=A1=9C=20=EC=B6=94=EC=B6=9C=20(=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EB=8F=99=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeckHud/DeckInspect/DeckAll/Combat/Reward/Map/Shop/Rest/Treasure/JobChoice/ JobSelect/Lobby/Board/SoulShop를 각 build 함수로 분리, upsertUi는 emit 한 줄로. 전문 상수(PANEL_BG·TYPE_KO)는 해당 블록에 포함. 산출물 무변경(diffcheck IDENTICAL). Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/deck/gen-slaydeck.mjs | 1829 +------------------------------- tools/deck/hud/board.mjs | 58 + tools/deck/hud/combat.mjs | 494 +++++++++ tools/deck/hud/deckall.mjs | 159 +++ tools/deck/hud/deckhud.mjs | 121 +++ tools/deck/hud/deckinspect.mjs | 138 +++ tools/deck/hud/jobchoice.mjs | 51 + tools/deck/hud/jobselect.mjs | 89 ++ tools/deck/hud/lobby.mjs | 58 + tools/deck/hud/map.mjs | 162 +++ tools/deck/hud/rest.mjs | 61 ++ tools/deck/hud/reward.mjs | 98 ++ tools/deck/hud/shop.mjs | 203 ++++ tools/deck/hud/soulshop.mjs | 90 ++ tools/deck/hud/treasure.mjs | 89 ++ 15 files changed, 1899 insertions(+), 1801 deletions(-) create mode 100644 tools/deck/hud/board.mjs create mode 100644 tools/deck/hud/combat.mjs create mode 100644 tools/deck/hud/deckall.mjs create mode 100644 tools/deck/hud/deckhud.mjs create mode 100644 tools/deck/hud/deckinspect.mjs create mode 100644 tools/deck/hud/jobchoice.mjs create mode 100644 tools/deck/hud/jobselect.mjs create mode 100644 tools/deck/hud/lobby.mjs create mode 100644 tools/deck/hud/map.mjs create mode 100644 tools/deck/hud/rest.mjs create mode 100644 tools/deck/hud/reward.mjs create mode 100644 tools/deck/hud/shop.mjs create mode 100644 tools/deck/hud/soulshop.mjs create mode 100644 tools/deck/hud/treasure.mjs diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 8282454..79b1f1f 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -1,6 +1,20 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs'; +import { buildDeckHud } from './hud/deckhud.mjs'; +import { buildDeckInspect } from './hud/deckinspect.mjs'; +import { buildDeckAll } from './hud/deckall.mjs'; +import { buildCombat } from './hud/combat.mjs'; +import { buildReward } from './hud/reward.mjs'; +import { buildMap } from './hud/map.mjs'; +import { buildShop } from './hud/shop.mjs'; +import { buildRest } from './hud/rest.mjs'; +import { buildTreasure } from './hud/treasure.mjs'; +import { buildJobChoice } from './hud/jobchoice.mjs'; +import { buildJobSelect } from './hud/jobselect.mjs'; +import { buildLobby } from './hud/lobby.mjs'; +import { buildBoard } from './hud/board.mjs'; +import { buildSoulShop } from './hud/soulshop.mjs'; import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from './lib/ui-helpers.mjs'; function upsertUi() { @@ -167,1628 +181,29 @@ function upsertUi() { } } - const hud = []; - const add = (e) => hud.push(e); + emit('DeckHud', buildDeckHud()); - add(entity({ - id: guid('hud', 0), - path: '/ui/DefaultGroup/DeckHud', - modelId: 'uiempty', - entryId: 'UIEmpty', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 5, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }), - sprite({ color: TRANSPARENT }), - ], - })); + emit('DeckInspectHud', buildDeckInspect()); - for (const pile of [ - { key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } }, - { key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } }, - ]) { - add(entity({ - id: guid('hud', hud.length), - path: `/ui/DefaultGroup/DeckHud/${pile.key}`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: pile.key === 'DrawPile' ? 0 : 1, - components: [ - transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }), - sprite({ color: pile.color, type: 1, raycast: true }), - button(), - ], - })); - add(entity({ - id: guid('hud', hud.length), - path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }), - sprite({ color: TRANSPARENT }), - text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }), - ], - })); - add(entity({ - id: guid('hud', hud.length), - path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }), - sprite({ color: TRANSPARENT }), - text({ value: pile.count, fontSize: 42, bold: true }), - ], - })); - } + emit('DeckAllHud', buildDeckAll()); - add(entity({ - id: guid('hud', hud.length), - path: '/ui/DefaultGroup/DeckHud/EndTurnButton', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }), - sprite({ color: DARK, type: 1, raycast: true }), - button(), - text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }), - ], - })); + emit('CombatHud', buildCombat()); - add(entity({ - id: guid('hud', hud.length), - path: '/ui/DefaultGroup/DeckHud/EnergyOrb', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 3, - components: [ - transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }), - ], - })); - add(entity({ - id: guid('hud', hud.length), - path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }), - sprite({ color: TRANSPARENT }), - text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }), - ], - })); - add(entity({ - id: guid('hud', hud.length), - path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }), - sprite({ color: TRANSPARENT }), - text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }), - ], - })); + emit('RewardHud', buildReward()); - emit('DeckHud', hud); + emit('MapHud', buildMap()); - const inspect = []; - const inspectHud = entity({ - id: guid('ins', 0), - path: '/ui/DefaultGroup/DeckInspectHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 15, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }), - ], - }); - inspectHud.jsonString.enable = false; - inspect.push(inspectHud); - inspect.push(entity({ - id: guid('ins', 1), - path: '/ui/DefaultGroup/DeckInspectHud/Panel', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }), - ], - })); - inspect.push(entity({ - id: guid('ins', 2), - path: '/ui/DefaultGroup/DeckInspectHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 54 }, pos: { x: 0, y: 350 } }), - sprite({ color: TRANSPARENT }), - text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }), - ], - })); - inspect.push(entity({ - id: guid('ins', 3), - path: '/ui/DefaultGroup/DeckInspectHud/Close', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 466, y: 350 } }), - sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), - ], - })); - inspect.push(entity({ - id: guid('ins', 4), - path: '/ui/DefaultGroup/DeckInspectHud/Empty', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 3, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }), - sprite({ color: TRANSPARENT }), - text({ value: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }), - ], - })); - inspect.push(entity({ - id: guid('ins', 5), - path: '/ui/DefaultGroup/DeckInspectHud/Grid', - modelId: 'uiempty', - entryId: 'UIEmpty', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent', - displayOrder: 4, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }), - sprite({ color: TRANSPARENT, type: 1, raycast: true }), - scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }), - ], - })); - const INSPECT_CARD_COUNT = 60; - const INSPECT_CARD_W = 158; - const INSPECT_CARD_H = 214; - // 카드 단위 엔티티는 v2 네임스페이스(ins2/all2/rwd2/shp2) — 자식 구성이 바뀌면 id를 통째로 새로 발급해야 함. - // 구 id를 다른 path에 재사용하면 메이커 refresh의 id 기준 in-place 병합이 꼬여 자식이 소실됨 (P13 실측). - for (let i = 1; i <= INSPECT_CARD_COUNT; i++) { - const insBase = 6 + (i - 1) * 7; - const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`; - const card = entity({ - id: guid('ins2', insBase), - path: cardPath, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: i, - components: [ - transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }), - sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }), - ], - }); - card.jsonString.enable = false; - inspect.push(card); - const inspectLayout = cardFaceLayout(INSPECT_CARD_W); - for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) { - const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; - inspect.push(entity({ - id: guid('ins2', insBase + 1 + tIdx), - path: `${cardPath}/${suffix}`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: dOrder, - components: [ - transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), - sprite({ color: TRANSPARENT }), - text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), - ], - })); - } - inspect.push(entity({ - id: guid('ins2', insBase + 6), - path: `${cardPath}/Art`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 5, - components: [ - transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }), - sprite({ color: WHITE, type: 0, raycast: false }), - ], - })); - } - emit('DeckInspectHud', inspect); + emit('ShopHud', buildShop()); - const allDeck = []; - const allHud = entity({ - id: guid('all', 0), - path: '/ui/DefaultGroup/DeckAllHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 16, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }), - ], - }); - allHud.jsonString.enable = false; - allDeck.push(allHud); - allDeck.push(entity({ - id: guid('all', 1), - path: '/ui/DefaultGroup/DeckAllHud/Panel', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }), - ], - })); - allDeck.push(entity({ - id: guid('all', 2), - path: '/ui/DefaultGroup/DeckAllHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 54 }, pos: { x: 0, y: 380 } }), - sprite({ color: TRANSPARENT }), - text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }), - ], - })); - allDeck.push(entity({ - id: guid('all', 3), - path: '/ui/DefaultGroup/DeckAllHud/Close', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 486, y: 380 } }), - sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), - ], - })); - const deckTabs = [ - { key: 'Warrior', label: '전사', x: -210 }, - { key: 'Thief', label: '도적', x: 0 }, - { key: 'Mage', label: '마법사', x: 210 }, - ]; - for (let i = 0; i < deckTabs.length; i++) { - const tab = deckTabs[i]; - allDeck.push(entity({ - id: guid('all', 10 + i), - path: `/ui/DefaultGroup/DeckAllHud/${tab.key}Tab`, - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 3 + i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 170, y: 46 }, pos: { x: tab.x, y: 318 } }), - sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: tab.label, fontSize: 22, bold: true, color: GOLD, alignment: 4 }), - ], - })); - } - allDeck.push(entity({ - id: guid('all', 4), - path: '/ui/DefaultGroup/DeckAllHud/Empty', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 3, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 40 } }), - sprite({ color: TRANSPARENT }), - text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }), - ], - })); - allDeck.push(entity({ - id: guid('all', 5), - path: '/ui/DefaultGroup/DeckAllHud/Grid', - modelId: 'uiempty', - entryId: 'UIEmpty', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent', - displayOrder: 4, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }), - sprite({ color: TRANSPARENT, type: 1, raycast: true }), - scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }), - ], - })); - const ALL_DECK_CARD_COUNT = 120; - const ALL_DECK_CARD_W = 158; - const ALL_DECK_CARD_H = 214; - // 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조 - for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) { - const allBase = 6 + (i - 1) * 7; - const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`; - const card = entity({ - id: guid('all2', allBase), - path: cardPath, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: i, - components: [ - transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }), - sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }), - ], - }); - card.jsonString.enable = false; - allDeck.push(card); - const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W); - for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) { - const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; - allDeck.push(entity({ - id: guid('all2', allBase + 1 + tIdx), - path: `${cardPath}/${suffix}`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: dOrder, - components: [ - transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), - sprite({ color: TRANSPARENT }), - text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), - ], - })); - } - allDeck.push(entity({ - id: guid('all2', allBase + 6), - path: `${cardPath}/Art`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 5, - components: [ - transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }), - sprite({ color: WHITE, type: 0, raycast: false }), - ], - })); - } - emit('DeckAllHud', allDeck); - - const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 }; - const combat = []; - combat.push(entity({ - id: guid('cmb', 0), - path: '/ui/DefaultGroup/CombatHud', - modelId: 'uiempty', - entryId: 'UIEmpty', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 4, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT }), - ], - })); - const SLOT_W = 140, SLOT_H = 96; - for (let i = 1; i <= MAX_MONSTERS; i++) { - const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`; - const slot = entity({ - id: guid('cmb', 40 + i), - path: base, - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 20 + i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }), - sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }), - button(), - ], - }); - slot.jsonString.enable = false; - combat.push(slot); - const targetFrame = entity({ - id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - 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 + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }), - sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }), - ], - }); - targetFrame.jsonString.enable = false; - combat.push(targetFrame); - const targetMarker = entity({ - id: guid('cmb', 360 + i), path: `${base}/TargetMarker`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 9, - components: [ - transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 116, y: 116 }, pos: { x: 0, y: 2 } }), - sprite({ color: { r: 0.95, g: 0.08, b: 0.05, a: 0.92 }, type: 1 }), - text({ value: '+', fontSize: 72, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 4 }), - ], - }); - targetMarker.jsonString.enable = false; - combat.push(targetMarker); - const targetLabel = entity({ - id: guid('cmb', 370 + i), path: `${base}/TargetMarker/Label`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 10, - components: [ - transform({ parentW: 116, parentH: 116, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 112, y: 28 }, pos: { x: 0, y: -52 } }), - sprite({ color: { r: 0.08, g: 0.02, b: 0.02, a: 0.86 }, type: 1 }), - text({ value: 'TARGET', fontSize: 18, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 3 }), - ], - }); - targetLabel.jsonString.enable = false; - combat.push(targetLabel); - const actFrame = entity({ - id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - 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 + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }), - sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }), - ], - }); - actFrame.jsonString.enable = false; - combat.push(actFrame); - combat.push(entity({ - id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - 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, y: 30 }, pos: { x: 0, y: 34 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - 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, y: 26 }, pos: { x: 0, y: 6 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 3, - components: [ - transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }), - sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 4, - components: [ - transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }), - sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 5, - 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 + 40, y: 24 }, pos: { x: 0, y: -36 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }), - ], - })); - const dmgPopBase = `/ui/DefaultGroup/CombatHud/DmgPop${i}`; - const dmgPop = entity({ - id: guid('cmb', 250 + i), path: dmgPopBase, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 80 + i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 192, y: 56 }, pos: { x: 0, y: 0 } }), - sprite({ color: TRANSPARENT, type: 1 }), - ], - }); - dmgPop.jsonString.enable = false; - combat.push(dmgPop); - for (let d = 0; d < DAMAGE_POP_MAX_DIGITS; d++) { - combat.push(entity({ - id: guid('cmb', 380 + i * 10 + d), path: `${dmgPopBase}/Digit${d + 1}`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 90 + i, - components: [ - transform({ parentW: 192, parentH: 56, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: DAMAGE_POP_DIGIT_W, y: DAMAGE_POP_DIGIT_H }, pos: { x: 0, y: 0 } }), - sprite({ dataId: DAMAGE_DIGIT_RUIDS[0], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0 }), - ], - })); - } - 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 }), - ], - })); - } - const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel'; - combat.push(entity({ - id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 5, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }), - sprite({ color: PANEL_BG, type: 1 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }), - sprite({ color: TRANSPARENT }), - text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 1, - components: [ - transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }), - sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 2, - components: [ - transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }), - sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 3, - components: [ - transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }), - sprite({ color: TRANSPARENT }), - text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - const blockBadge = entity({ - id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 4, - components: [ - transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }), - sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }), - ], - }); - blockBadge.jsonString.enable = false; - combat.push(blockBadge); - combat.push(entity({ - id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }), - sprite({ color: TRANSPARENT }), - text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - 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 }), - ], - })); - const playerDmgPop = entity({ - id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 5, - components: [ - transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }), - ], - }); - playerDmgPop.jsonString.enable = false; - combat.push(playerDmgPop); - combat.push(entity({ - id: guid('cmb', 200), - path: '/ui/DefaultGroup/CombatHud/TopBar', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 9, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }), - ], - })); - const topTexts = [ - ['Floor', -520, 160, '막 1/3', GOLD], - ['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }], - ]; - topTexts.forEach(([suffix, x, w, value, color], ti) => { - combat.push(entity({ - id: guid('cmb', 201 + ti), - path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`, - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: ti, - components: [ - transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }), - sprite({ color: TRANSPARENT }), - text({ value, fontSize: 22, bold: true, color, alignment: 4 }), - ], - })); - }); - combat.push(entity({ - id: guid('cmb', 209), - path: '/ui/DefaultGroup/CombatHud/TopBar/MesoIcon', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 26, y: 26 }, pos: { x: -432, y: 0 } }), - sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }), - ], - })); - for (let i = 1; i <= 10; i++) { - combat.push(entity({ - id: guid('cmb', 300 + i), - path: `/ui/DefaultGroup/CombatHud/TopBar/RelicSlot${i}`, - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent', - displayOrder: 3 + i, - components: [ - transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: -240 + (i - 1) * 48, y: 0 } }), - sprite({ color: { r: 0.15, g: 0.16, b: 0.2, a: 0.6 }, type: 0, raycast: true }), - { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, - ], - })); - } - combat.push(entity({ - id: guid('cmb', 311), - path: '/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 14, - components: [ - transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 60, y: 30 }, pos: { x: 192, y: 0 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 18, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }), - ], - })); - for (let i = 1; i <= 5; i++) { - combat.push(entity({ - id: guid('cmb', 320 + i), - path: `/ui/DefaultGroup/CombatHud/TopBar/PotionSlot${i}`, - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent', - displayOrder: 14 + i, - components: [ - transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: 240 + (i - 1) * 44, y: 0 } }), - sprite({ color: { r: 0.22, g: 0.25, b: 0.3, a: 0.9 }, type: 0, raycast: true }), - { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, - ], - })); - } - const tooltipBox = entity({ - id: guid('cmb', 330), - path: '/ui/DefaultGroup/CombatHud/TooltipBox', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 20, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 150 }, pos: { x: 0, y: 400 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.04, g: 0.05, b: 0.08, a: 0.96 }, type: 1 }), - ], - }); - tooltipBox.jsonString.enable = false; - combat.push(tooltipBox); - combat.push(entity({ - id: guid('cmb', 331), - path: '/ui/DefaultGroup/CombatHud/TooltipBox/Name', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 28 }, pos: { x: 0, y: 52 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 19, bold: true, color: GOLD, alignment: 4 }), - ], - })); - combat.push(entity({ - id: guid('cmb', 332), - path: '/ui/DefaultGroup/CombatHud/TooltipBox/Desc', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 102 }, pos: { x: 0, y: -18 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 0 }), - ], - })); - const discardPrompt = entity({ - id: guid('cmb', 333), - path: '/ui/DefaultGroup/CombatHud/DiscardPrompt', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 22, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 520, y: 48 }, pos: { x: 0, y: -260 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.86 }, type: 1 }), - text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), - ], - }); - discardPrompt.jsonString.enable = false; - combat.push(discardPrompt); - const potionMenu = entity({ - id: guid('cmb', 340), - path: '/ui/DefaultGroup/CombatHud/PotionMenu', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 21, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 180 }, pos: { x: 0, y: 120 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.07, g: 0.08, b: 0.12, a: 0.97 }, type: 1 }), - ], - }); - potionMenu.jsonString.enable = false; - combat.push(potionMenu); - combat.push(entity({ - id: guid('cmb', 341), - path: '/ui/DefaultGroup/CombatHud/PotionMenu/Title', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 36 }, pos: { x: 0, y: 52 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 19, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - const pmButtons = [ - ['Use', '사용', -120, { r: 0.32, g: 0.55, b: 0.36, a: 1 }], - ['Toss', '버리기', 0, { r: 0.6, g: 0.32, b: 0.3, a: 1 }], - ['Close', '닫기', 120, { r: 0.25, g: 0.28, b: 0.35, a: 1 }], - ]; - pmButtons.forEach(([suffix, label, x, color], bi) => { - combat.push(entity({ - id: guid('cmb', 342 + bi), - path: `/ui/DefaultGroup/CombatHud/PotionMenu/${suffix}`, - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 1 + bi, - components: [ - transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 104, y: 46 }, pos: { x, y: -40 } }), - sprite({ color, type: 1, raycast: true }), - button(), - text({ value: label, fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - }); - combat.push(entity({ - id: guid('cmb', 205), - path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton', - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 3, - components: [ - transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 140, y: 40 }, pos: { x: 528, y: 0 } }), - sprite({ color: DARK, type: 1, raycast: true }), - button(), - text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }), - ], - })); - const skillFx = entity({ - id: guid('cmb', 230), path: '/ui/DefaultGroup/CombatHud/SkillFx', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 30, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 110, y: 110 }, pos: { x: 0, y: 0 } }), - sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), - ], - }); - skillFx.jsonString.enable = false; - combat.push(skillFx); - const result = entity({ - id: guid('cmb', 2), - path: '/ui/DefaultGroup/CombatHud/Result', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 8, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }), - ], - }); - result.jsonString.enable = false; - combat.push(result); - emit('CombatHud', combat); - - const reward = []; - const rewardHud = entity({ - id: guid('rwd', 0), - path: '/ui/DefaultGroup/RewardHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 6, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.86 }, type: 1, raycast: true }), - ], - }); - rewardHud.jsonString.enable = false; - reward.push(rewardHud); - reward.push(entity({ - id: guid('rwd', 1), - path: '/ui/DefaultGroup/RewardHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 64 }, pos: { x: 0, y: 300 } }), - sprite({ color: TRANSPARENT }), - text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), - ], - })); - const rewardXs = [-300, 0, 300]; - // 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조 - for (let i = 1; i <= 3; i++) { - const rwdBase = 2 + (i - 1) * 7; - const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`; - reward.push(entity({ - id: guid('rwd2', rwdBase), - path: cardPath, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent', - displayOrder: i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }), - sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }), - button(), - { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, - ], - })); - const rewardLayout = cardFaceLayout(CARD_W); - for (const [tIdx, [suffix, cfg]] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]).entries()) { - const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; - reward.push(entity({ - id: guid('rwd2', rwdBase + 1 + tIdx), - path: `${cardPath}/${suffix}`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: dOrder, - components: [ - transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), - sprite({ color: TRANSPARENT }), - text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), - ], - })); - } - reward.push(entity({ - id: guid('rwd2', rwdBase + 6), - path: `${cardPath}/Art`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 5, - components: [ - transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: rewardLayout.art.size, pos: rewardLayout.art.pos }), - sprite({ color: WHITE, type: 0, raycast: false }), - ], - })); - } - let rwdN = 2 + 3 * 7; // 구 시퀀스의 루프 종료 시점 값(23) 보존 — Skip 등 후속 id 불변 - reward.push(entity({ - id: guid('rwd', rwdN++), - path: '/ui/DefaultGroup/RewardHud/Skip', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 10, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -260 } }), - sprite({ color: DARK, type: 1, raycast: true }), - button(), - text({ value: '건너뛰기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), - ], - })); - emit('RewardHud', reward); - - const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' }; - const map = []; - const mapHud = entity({ - id: guid('map', 0), - path: '/ui/DefaultGroup/MapHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 7, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - // 불투명 다크 배경(게임 월드 가림 보장). 그 위 BgImage 자식이 배경 스프라이트를 얹는다. - sprite({ color: { r: 0.06, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }), - ], - }); - mapHud.jsonString.enable = false; - map.push(mapHud); - // 배경 이미지(displayOrder 0 = 도트/타이틀/노드 아래). nodeicons.json background는 SPRITE RUID여야 렌더됨 - // — 메이플 BackgroundComponent 리소스는 UI 스프라이트로 안 뜬다. 유효 스프라이트면 풀스크린 표시, 아니면 투명(다크 배경 노출). - map.push(entity({ - id: guid('map', 990), - path: '/ui/DefaultGroup/MapHud/BgImage', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ dataId: NODEICONS.background, color: { r: 0.5, g: 0.52, b: 0.58, a: 1 }, type: 0, raycast: false }), - ], - })); - map.push(entity({ - id: guid('map', 1), - path: '/ui/DefaultGroup/MapHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 510 } }), - sprite({ color: TRANSPARENT }), - text({ value: '다음 노드 선택', fontSize: 40, bold: true, color: GOLD, alignment: 4 }), - ], - })); - // 절차 생성 맵용 정적 그리드 — 가로 진행(왼→오른쪽): 행(row)=x축, 열(col)=y축 분기, 보스는 최우측 중앙. - const nodeX = (row) => -540 + (row - 1) * 150; - const nodeY = (col) => 180 - (col - 1) * 120; - const BOSS_POS = { x: nodeX(MAP_ROWS) + 150, y: 0 }; - let mapN = 2; - const pushMapNode = (id, pos, size, label) => { - const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`; - const nodeEnt = entity({ - id: guid('map', mapN++), - path: nodePath, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 5, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }), - sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }), - button(), - ], - }); - nodeEnt.jsonString.enable = false; - map.push(nodeEnt); - }; - for (let r = 1; r <= MAP_ROWS; r++) { - for (let c = 1; c <= MAP_COLS; c++) { - pushMapNode(`r${r}c${c}`, { x: nodeX(r), y: nodeY(c) }, { x: 64, y: 64 }, ''); - } - } - pushMapNode('boss', BOSS_POS, { x: 88, y: 88 }, '보스'); - const pushDots = (dotId, from, to) => { - for (let k = 1; k <= 3; k++) { - const t = k / 4; - const dot = entity({ - id: guid('map', mapN++), - path: `/ui/DefaultGroup/MapHud/Dot_${dotId}_${k}`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 8, y: 8 }, pos: { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t } }), - sprite({ color: { r: 0.5, g: 0.5, b: 0.55, a: 0.8 }, type: 1 }), - ], - }); - dot.jsonString.enable = false; - map.push(dot); - } - }; - for (let r = 1; r < MAP_ROWS; r++) { - for (let c = 1; c <= MAP_COLS; c++) { - for (let c2 = c - 1; c2 <= c + 1; c2++) { - if (c2 < 1 || c2 > MAP_COLS) continue; - pushDots(`r${r}c${c}_${c2}`, { x: nodeX(r), y: nodeY(c) }, { x: nodeX(r + 1), y: nodeY(c2) }); - } - } - } - for (let c = 1; c <= MAP_COLS; c++) { - pushDots(`r${MAP_ROWS}c${c}_b`, { x: nodeX(MAP_ROWS), y: nodeY(c) }, BOSS_POS); - } - // 노드 종류 범례 (우측 하단) — 각 타입 아이콘 + 이름 - const LEGEND = [['combat', '전투'], ['elite', '엘리트'], ['boss', '보스'], ['shop', '상점'], ['rest', '휴식'], ['treasure', '보물']]; - const lgW = 300, lgH = 312; - map.push(entity({ - id: guid('map', 991), - path: '/ui/DefaultGroup/MapHud/Legend', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 4, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: lgW, y: lgH }, pos: { x: 760, y: -334 } }), - sprite({ color: { r: 0.08, g: 0.09, b: 0.14, a: 0.86 }, type: 1, raycast: false }), - ], - })); - map.push(entity({ - id: guid('map', 992), - path: '/ui/DefaultGroup/MapHud/Legend/LegendTitle', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: lgW, parentH: lgH, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: lgW - 20, y: 32 }, pos: { x: 0, y: lgH / 2 - 26 } }), - sprite({ color: TRANSPARENT }), - text({ value: '노드 종류', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), - ], - })); - let lgId = 993; - LEGEND.forEach(([t, ko], i) => { - const rowY = lgH / 2 - 78 - i * 38; - map.push(entity({ - id: guid('map', lgId++), - path: `/ui/DefaultGroup/MapHud/Legend/Icon_${t}`, - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 2, - components: [ - transform({ parentW: lgW, parentH: lgH, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 36, y: 36 }, pos: { x: -lgW / 2 + 38, y: rowY } }), - sprite({ dataId: NODEICONS.icons[t], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), - ], - })); - map.push(entity({ - id: guid('map', lgId++), - path: `/ui/DefaultGroup/MapHud/Legend/Label_${t}`, - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: lgW, parentH: lgH, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: lgW - 110, y: 30 }, pos: { x: 32, y: rowY } }), - sprite({ color: TRANSPARENT }), - text({ value: ko, fontSize: 19, bold: false, color: { r: 0.9, g: 0.92, b: 0.96, a: 1 }, alignment: 4 }), - ], - })); - }); - emit('MapHud', map); - - const shop = []; - const shopHud = entity({ - id: guid('shp', 0), - path: '/ui/DefaultGroup/ShopHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 8, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), - ], - }); - shopHud.jsonString.enable = false; - shop.push(shopHud); - shop.push(entity({ - id: guid('shp', 1), - path: '/ui/DefaultGroup/ShopHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }), - sprite({ color: TRANSPARENT }), - text({ value: '상점', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), - ], - })); - shop.push(entity({ - id: guid('shp', 2), - path: '/ui/DefaultGroup/ShopHud/Gold', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }), - sprite({ color: TRANSPARENT }), - text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), - ], - })); - shop.push(entity({ - id: guid('shp', 3), - path: '/ui/DefaultGroup/ShopHud/MesoIcon', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 30, y: 30 }, pos: { x: -86, y: 330 } }), - sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }), - ], - })); - const shopXs = [-300, 0, 300]; - // 카드 단위 엔티티 v2 네임스페이스 (stride 8: Price 포함) — DeckInspectHud 주석 참조 - for (let i = 1; i <= 3; i++) { - const shpBase = 3 + (i - 1) * 8; - const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`; - shop.push(entity({ - id: guid('shp2', shpBase), - path: cardPath, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent', - displayOrder: i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }), - sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }), - button(), - { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, - ], - })); - const shopLayout = cardFaceLayout(CARD_W); - const shopTexts = shopLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]); - shopTexts.push(['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -135 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }]); - for (const [tIdx, [suffix, cfg]] of shopTexts.entries()) { - const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9; - shop.push(entity({ - id: guid('shp2', shpBase + 1 + tIdx), - path: `${cardPath}/${suffix}`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: dOrder, - components: [ - transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), - sprite({ color: TRANSPARENT }), - text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), - ], - })); - } - shop.push(entity({ - id: guid('shp2', shpBase + 7), - path: `${cardPath}/Art`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 5, - components: [ - transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: shopLayout.art.size, pos: shopLayout.art.pos }), - sprite({ color: WHITE, type: 0, raycast: false }), - ], - })); - } - let shpN = 3 + 3 * 8; // 구 시퀀스의 루프 종료 시점 값(27) 보존 — Relic 등 후속 id 불변 - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Relic', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 9, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -190 } }), - sprite({ color: { r: 0.7, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }), - button(), - ], - })); - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Relic/Label', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }), - sprite({ color: TRANSPARENT }), - text({ value: '유물', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Relic/Price', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }), - sprite({ color: TRANSPARENT }), - text({ value: '60 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), - ], - })); - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Potion', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 11, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -278 } }), - sprite({ color: { r: 0.45, g: 0.7, b: 0.55, a: 1 }, type: 1, raycast: true }), - button(), - ], - })); - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Potion/Label', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }), - sprite({ color: TRANSPARENT }), - text({ value: '물약', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Potion/Price', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }), - sprite({ color: TRANSPARENT }), - text({ value: '20 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), - ], - })); - shop.push(entity({ - id: guid('shp', shpN++), - path: '/ui/DefaultGroup/ShopHud/Leave', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 10, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 } }), - sprite({ color: DARK, type: 1, raycast: true }), - button(), - text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), - ], - })); - emit('ShopHud', shop); - - const rest = []; - const restHud = entity({ - id: guid('rst', 0), - path: '/ui/DefaultGroup/RestHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 9, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.08, b: 0.06, a: 0.92 }, type: 1, raycast: true }), - ], - }); - restHud.jsonString.enable = false; - rest.push(restHud); - rest.push(entity({ - id: guid('rst', 1), - path: '/ui/DefaultGroup/RestHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 140 } }), - sprite({ color: TRANSPARENT }), - text({ value: '휴식', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), - ], - })); - rest.push(entity({ - id: guid('rst', 2), - path: '/ui/DefaultGroup/RestHud/Info', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }), - sprite({ color: TRANSPARENT }), - text({ value: 'HP 회복', fontSize: 30, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - rest.push(entity({ - id: guid('rst', 3), - path: '/ui/DefaultGroup/RestHud/Leave', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -120 } }), - sprite({ color: DARK, type: 1, raycast: true }), - button(), - text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), - ], - })); - emit('RestHud', rest); + emit('RestHud', buildRest()); // 유물 방 — 보물 상자 (P8) - const treasure = []; - const treasureHud = entity({ - id: guid('trs', 0), - path: '/ui/DefaultGroup/TreasureHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 8, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), - ], - }); - treasureHud.jsonString.enable = false; - treasure.push(treasureHud); - treasure.push(entity({ - id: guid('trs', 1), - path: '/ui/DefaultGroup/TreasureHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 320 } }), - sprite({ color: TRANSPARENT }), - text({ value: '보물 상자', fontSize: 40, bold: true, color: GOLD, alignment: 4 }), - ], - })); - treasure.push(entity({ - id: guid('trs', 2), - path: '/ui/DefaultGroup/TreasureHud/Chest', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 180 }, pos: { x: 0, y: 40 } }), - sprite({ dataId: CHEST_CLOSED_RUID, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }), - button(), - ], - })); - treasure.push(entity({ - id: guid('trs', 3), - path: '/ui/DefaultGroup/TreasureHud/Hint', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 500, y: 34 }, pos: { x: 0, y: -90 } }), - sprite({ color: TRANSPARENT }), - text({ value: '상자를 클릭해 여세요', fontSize: 20, bold: false, color: { r: 0.85, g: 0.85, b: 0.9, a: 1 }, alignment: 4 }), - ], - })); - const treasureReward = entity({ - id: guid('trs', 4), - path: '/ui/DefaultGroup/TreasureHud/Reward', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 3, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 44 }, pos: { x: 0, y: -160 } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), - ], - }); - treasureReward.jsonString.enable = false; - treasure.push(treasureReward); - treasure.push(entity({ - id: guid('trs', 5), - path: '/ui/DefaultGroup/TreasureHud/Leave', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 4, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -280 } }), - sprite({ color: DARK, type: 1, raycast: true }), - button(), - text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), - ], - })); - emit('TreasureHud', treasure); + emit('TreasureHud', buildTreasure()); // 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직 - const jobChoice = []; - const jobChoiceHud = entity({ - id: guid('job', 0), - path: '/ui/DefaultGroup/JobChoiceHud', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 9, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), - ], - }); - jobChoiceHud.jsonString.enable = false; - jobChoice.push(jobChoiceHud); - jobChoice.push(entity({ - id: guid('job', 1), - path: '/ui/DefaultGroup/JobChoiceHud/Title', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 220 } }), - sprite({ color: TRANSPARENT }), - text({ value: '보스 처치 보상을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), - ], - })); - const jcButtons = [ - ['RelicButton', '유물 획득', -240, { r: 0.7, g: 0.55, b: 0.85, a: 1 }], - ['JobButton', '2차 전직', 240, { r: 0.86, g: 0.6, b: 0.3, a: 1 }], - ]; - jcButtons.forEach(([suffix, label, x, color], bi) => { - jobChoice.push(entity({ - id: guid('job', 2 + bi), - path: `/ui/DefaultGroup/JobChoiceHud/${suffix}`, - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 1 + bi, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 140 }, pos: { x, y: 0 } }), - sprite({ color, type: 1, raycast: true }), - button(), - text({ value: label, fontSize: 32, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - }); - emit('JobChoiceHud', jobChoice); + emit('JobChoiceHud', buildJobChoice()); - const jobSelect = []; - const jobSelectHud = entity({ - id: guid('job', 10), - path: '/ui/DefaultGroup/JobSelectHud', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 10, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.94 }, type: 1, raycast: true }), - ], - }); - jobSelectHud.jsonString.enable = false; - jobSelect.push(jobSelectHud); - jobSelect.push(entity({ - id: guid('job', 11), - path: '/ui/DefaultGroup/JobSelectHud/Title', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 300 } }), - sprite({ color: TRANSPARENT }), - text({ value: '2차 전직 — 직업을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), - ], - })); - // 범용 슬롯 3개 — ShowJobSelect(Lua)가 클래스별 JOBS로 텍스트를 채움 (P10 동적화) - const jobs = [ - ['slot1', '', '', '', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }], - ['slot2', '', '', '', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }], - ['slot3', '', '', '', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }], - ]; - jobs.forEach(([jobId, name, desc, starter, x, color], ji) => { - const base = `/ui/DefaultGroup/JobSelectHud/Job_${jobId}`; - jobSelect.push(entity({ - id: guid('job', 12 + ji * 4), - path: base, - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 1 + ji, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 420 }, pos: { x, y: -20 } }), - sprite({ color, type: 1, raycast: true }), - button(), - ], - })); - jobSelect.push(entity({ - id: guid('job', 13 + ji * 4), - path: `${base}/Name`, - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 50 }, pos: { x: 0, y: 150 } }), - sprite({ color: TRANSPARENT }), - text({ value: name, fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), - ], - })); - jobSelect.push(entity({ - id: guid('job', 14 + ji * 4), - path: `${base}/Desc`, - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 160 }, pos: { x: 0, y: 0 } }), - sprite({ color: TRANSPARENT }), - text({ value: desc, fontSize: 22, bold: false, color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, alignment: 4 }), - ], - })); - jobSelect.push(entity({ - id: guid('job', 15 + ji * 4), - path: `${base}/Starter`, - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 32 }, pos: { x: 0, y: -160 } }), - sprite({ color: TRANSPARENT }), - text({ value: starter, fontSize: 18, bold: true, color: GOLD, alignment: 4 }), - ], - })); - }); - emit('JobSelectHud', jobSelect); + emit('JobSelectHud', buildJobSelect()); const menu = []; menu.push(entity({ @@ -2094,201 +509,13 @@ function upsertUi() { emit('CharacterSelectHud', select); // ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ── - const lobby = []; - let lobId = 0; - const lobbyRoot = entity({ - id: guid('lob', lobId++), - path: '/ui/DefaultGroup/LobbyHud', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 11, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - // 로비가 물리 맵이 됨 — 투명 + 비레이캐스트로 맵을 가리지 않고 월드 NPC 클릭이 통과되게 함. - sprite({ color: TRANSPARENT, type: 1, raycast: false }), - ], - }); - lobbyRoot.jsonString.enable = false; - lobby.push(lobbyRoot); - const lobTexts = [ - ['Title', 0, 478, 760, '메이플 로비', 40, GOLD], - ['SoulLabel', 700, 478, 320, '영혼 0', 28, { r: 0.6, g: 0.85, b: 1, a: 1 }], - ['AscLabel', -560, 478, 380, '승천 0 / 해금 0', 22, { r: 0.9, g: 0.7, b: 0.5, a: 1 }], - ['Hint', 0, -478, 1500, 'NPC에게 다가가 ↑ 또는 클릭으로 대화 · ← → 이동 · Ctrl 공격', 20, { r: 0.72, g: 0.76, b: 0.82, a: 1 }], - ]; - for (const [suffix, x, y, w, value, fs, color] of lobTexts) { - lobby.push(entity({ - id: guid('lob', lobId++), - path: `/ui/DefaultGroup/LobbyHud/${suffix}`, - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 56 }, pos: { x, y } }), - sprite({ color: TRANSPARENT }), - text({ value, fontSize: fs, bold: true, color, alignment: 4 }), - ], - })); - } - for (const [suffix, x, label] of [['AscMinus', -780, '<'], ['AscPlus', -540, '>']]) { - lobby.push(entity({ - id: guid('lob', lobId++), - path: `/ui/DefaultGroup/LobbyHud/${suffix}`, - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 46, y: 42 }, pos: { x, y: 470 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: label, fontSize: 28, bold: true, color: WHITE, alignment: 4 }), - ], - })); - } - // NPC 4종은 로비 물리 맵의 월드 엔티티(map/lobby.map + LobbyNpc codeblock)로 이동. UI 버튼 행 제거. - emit('LobbyHud', lobby); + emit('LobbyHud', buildLobby()); // ── BoardHud — 게시판(공지/팁) ── - const board = []; - let brdId = 0; - const boardRoot = entity({ - id: guid('brd', brdId++), - path: '/ui/DefaultGroup/BoardHud', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 14, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.95 }, type: 1, raycast: true }), - ], - }); - boardRoot.jsonString.enable = false; - board.push(boardRoot); - board.push(entity({ - id: guid('brd', brdId++), - path: '/ui/DefaultGroup/BoardHud/Title', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }), - sprite({ color: TRANSPARENT }), - text({ value: '게시판', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), - ], - })); - board.push(entity({ - id: guid('brd', brdId++), - path: '/ui/DefaultGroup/BoardHud/Body', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1100, y: 520 }, pos: { x: 0, y: 20 } }), - sprite({ color: { r: 0.1, g: 0.12, b: 0.16, a: 0.9 }, type: 1 }), - text({ value: '· 카드는 직업/등급에 따라 보상 확률이 다릅니다.\n· 몬스터는 매 턴 정해진 행동 중 하나를 무작위로 합니다.\n· 일부 몬스터는 덱에 저주 카드(상처/화상)를 넣습니다.\n· 손패는 최대 10장, 초과분은 자동으로 버려집니다.\n· 2차 전직 후 보스를 잡으면 영혼이 쌓입니다.\n· 영혼은 상인 NPC에서 덱빌딩 해금에 사용합니다.', fontSize: 24, bold: false, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }), - ], - })); - board.push(entity({ - id: guid('brd', brdId++), - path: '/ui/DefaultGroup/BoardHud/Close', - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }), - ], - })); - emit('BoardHud', board); + emit('BoardHud', buildBoard()); // ── SoulShopHud — 영혼 메타 상점 (Phase 9에서 해금 항목·구매 로직 채움) ── - const soulShop = []; - let soulId = 0; - const soulRoot = entity({ - id: guid('soul', soulId++), - path: '/ui/DefaultGroup/SoulShopHud', - modelId: 'uisprite', entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 15, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.95 }, type: 1, raycast: true }), - ], - }); - soulRoot.jsonString.enable = false; - soulShop.push(soulRoot); - soulShop.push(entity({ - id: guid('soul', soulId++), - path: '/ui/DefaultGroup/SoulShopHud/Title', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 410 } }), - sprite({ color: TRANSPARENT }), - text({ value: '영혼 상점', fontSize: 44, bold: true, color: { r: 0.6, g: 0.85, b: 1, a: 1 }, alignment: 4 }), - ], - })); - soulShop.push(entity({ - id: guid('soul', soulId++), - path: '/ui/DefaultGroup/SoulShopHud/Souls', - modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 400, y: 44 }, pos: { x: 0, y: 345 } }), - sprite({ color: TRANSPARENT }), - text({ value: '영혼 0', fontSize: 28, bold: true, color: { r: 0.6, g: 0.85, b: 1, a: 1 }, alignment: 4 }), - ], - })); - soulShop.push(entity({ - id: guid('soul', soulId++), - path: '/ui/DefaultGroup/SoulShopHud/Close', - modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -400 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }), - ], - })); - for (let i = 1; i <= 4; i++) { - const ip = `/ui/DefaultGroup/SoulShopHud/Item${i}`; - const iy = 230 - (i - 1) * 125; - soulShop.push(entity({ - id: guid('soul', soulId++), - path: ip, modelId: 'uibutton', entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 104 }, pos: { x: 0, y: iy }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.14, g: 0.16, b: 0.22, a: 1 }, type: 1, raycast: true }), - button(), - ], - })); - for (const [suffix, x, y, w, fs, color] of [ - ['Name', -180, 22, 360, 28, GOLD], - ['Desc', -180, -24, 440, 20, { r: 0.86, g: 0.9, b: 0.94, a: 1 }], - ['Status', 270, 0, 220, 22, { r: 0.6, g: 0.85, b: 1, a: 1 }], - ]) { - soulShop.push(entity({ - id: guid('soul', soulId++), - path: `${ip}/${suffix}`, modelId: 'uitext', entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 760, parentH: 104, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 38 }, pos: { x, y } }), - sprite({ color: TRANSPARENT }), - text({ value: '', fontSize: fs, bold: suffix === 'Name', color, alignment: 4 }), - ], - })); - } - } - emit('SoulShopHud', soulShop); + emit('SoulShopHud', buildSoulShop()); for (const section of UI_APPEND_ORDER) { const entities = uiSections.get(section); diff --git a/tools/deck/hud/board.mjs b/tools/deck/hud/board.mjs new file mode 100644 index 0000000..299bc34 --- /dev/null +++ b/tools/deck/hud/board.mjs @@ -0,0 +1,58 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildBoard() { + const board = []; + let brdId = 0; + const boardRoot = entity({ + id: guid('brd', brdId++), + path: '/ui/DefaultGroup/BoardHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 14, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.95 }, type: 1, raycast: true }), + ], + }); + boardRoot.jsonString.enable = false; + board.push(boardRoot); + board.push(entity({ + id: guid('brd', brdId++), + path: '/ui/DefaultGroup/BoardHud/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }), + sprite({ color: TRANSPARENT }), + text({ value: '게시판', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), + ], + })); + board.push(entity({ + id: guid('brd', brdId++), + path: '/ui/DefaultGroup/BoardHud/Body', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1100, y: 520 }, pos: { x: 0, y: 20 } }), + sprite({ color: { r: 0.1, g: 0.12, b: 0.16, a: 0.9 }, type: 1 }), + text({ value: '· 카드는 직업/등급에 따라 보상 확률이 다릅니다.\n· 몬스터는 매 턴 정해진 행동 중 하나를 무작위로 합니다.\n· 일부 몬스터는 덱에 저주 카드(상처/화상)를 넣습니다.\n· 손패는 최대 10장, 초과분은 자동으로 버려집니다.\n· 2차 전직 후 보스를 잡으면 영혼이 쌓입니다.\n· 영혼은 상인 NPC에서 덱빌딩 해금에 사용합니다.', fontSize: 24, bold: false, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }), + ], + })); + board.push(entity({ + id: guid('brd', brdId++), + path: '/ui/DefaultGroup/BoardHud/Close', + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }), + ], + })); + return board; +} diff --git a/tools/deck/hud/combat.mjs b/tools/deck/hud/combat.mjs new file mode 100644 index 0000000..ae80a9b --- /dev/null +++ b/tools/deck/hud/combat.mjs @@ -0,0 +1,494 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildCombat() { + const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 }; + const combat = []; + combat.push(entity({ + id: guid('cmb', 0), + path: '/ui/DefaultGroup/CombatHud', + modelId: 'uiempty', + entryId: 'UIEmpty', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 4, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT }), + ], + })); + const SLOT_W = 140, SLOT_H = 96; + for (let i = 1; i <= MAX_MONSTERS; i++) { + const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`; + const slot = entity({ + id: guid('cmb', 40 + i), + path: base, + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 20 + i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }), + sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }), + button(), + ], + }); + slot.jsonString.enable = false; + combat.push(slot); + const targetFrame = entity({ + id: guid('cmb', 220 + i), path: `${base}/TargetFrame`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + 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 + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }), + sprite({ color: { r: 0.95, g: 0.78, b: 0.25, a: 0.28 }, type: 1 }), + ], + }); + targetFrame.jsonString.enable = false; + combat.push(targetFrame); + const targetMarker = entity({ + id: guid('cmb', 360 + i), path: `${base}/TargetMarker`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 9, + components: [ + transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 116, y: 116 }, pos: { x: 0, y: 2 } }), + sprite({ color: { r: 0.95, g: 0.08, b: 0.05, a: 0.92 }, type: 1 }), + text({ value: '+', fontSize: 72, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 4 }), + ], + }); + targetMarker.jsonString.enable = false; + combat.push(targetMarker); + const targetLabel = entity({ + id: guid('cmb', 370 + i), path: `${base}/TargetMarker/Label`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 10, + components: [ + transform({ parentW: 116, parentH: 116, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 112, y: 28 }, pos: { x: 0, y: -52 } }), + sprite({ color: { r: 0.08, g: 0.02, b: 0.02, a: 0.86 }, type: 1 }), + text({ value: 'TARGET', fontSize: 18, bold: true, color: { r: 1, g: 0.94, b: 0.28, a: 1 }, alignment: 4, outlineWidth: 3 }), + ], + }); + targetLabel.jsonString.enable = false; + combat.push(targetLabel); + const actFrame = entity({ + id: guid('cmb', 240 + i), path: `${base}/ActFrame`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + 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 + 16, y: SLOT_H + 12 }, pos: { x: 0, y: 0 } }), + sprite({ color: { r: 0.95, g: 0.3, b: 0.25, a: 0.3 }, type: 1 }), + ], + }); + actFrame.jsonString.enable = false; + combat.push(actFrame); + combat.push(entity({ + id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + 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, y: 30 }, pos: { x: 0, y: 34 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + 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, y: 26 }, pos: { x: 0, y: 6 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 3, + components: [ + transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }), + sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 4, + components: [ + transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }), + sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 5, + 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 + 40, y: 24 }, pos: { x: 0, y: -36 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }), + ], + })); + const dmgPopBase = `/ui/DefaultGroup/CombatHud/DmgPop${i}`; + const dmgPop = entity({ + id: guid('cmb', 250 + i), path: dmgPopBase, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 80 + i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 192, y: 56 }, pos: { x: 0, y: 0 } }), + sprite({ color: TRANSPARENT, type: 1 }), + ], + }); + dmgPop.jsonString.enable = false; + combat.push(dmgPop); + for (let d = 0; d < DAMAGE_POP_MAX_DIGITS; d++) { + combat.push(entity({ + id: guid('cmb', 380 + i * 10 + d), path: `${dmgPopBase}/Digit${d + 1}`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 90 + i, + components: [ + transform({ parentW: 192, parentH: 56, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: DAMAGE_POP_DIGIT_W, y: DAMAGE_POP_DIGIT_H }, pos: { x: 0, y: 0 } }), + sprite({ dataId: DAMAGE_DIGIT_RUIDS[0], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0 }), + ], + })); + } + 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 }), + ], + })); + } + const PP = '/ui/DefaultGroup/CombatHud/PlayerPanel'; + combat.push(entity({ + id: guid('cmb', 210), path: PP, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 5, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 96 }, pos: { x: -760, y: -494 }, align: ALIGN_CENTER }), + sprite({ color: PANEL_BG, type: 1 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 211), path: `${PP}/Name`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 280, y: 28 }, pos: { x: 0, y: 28 } }), + sprite({ color: TRANSPARENT }), + text({ value: '플레이어', fontSize: 18, bold: true, color: GOLD, alignment: 4 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 212), path: `${PP}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 1, + components: [ + transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 16 }, pos: { x: 16, y: -6 } }), + sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 213), path: `${PP}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 2, + components: [ + transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: 220, y: 14 }, pos: { x: -94, y: -6 } }), + sprite({ color: { r: 0.3, g: 0.78, b: 0.36, a: 1 }, type: 1 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 214), path: `${PP}/HpText`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 3, + components: [ + transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 24 }, pos: { x: 16, y: -30 } }), + sprite({ color: TRANSPARENT }), + text({ value: '80/80', fontSize: 16, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + const blockBadge = entity({ + id: guid('cmb', 215), path: `${PP}/BlockBadge`, modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 4, + components: [ + transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 40 }, pos: { x: -122, y: -12 } }), + sprite({ color: { r: 0.32, g: 0.5, b: 0.85, a: 1 }, type: 1 }), + ], + }); + blockBadge.jsonString.enable = false; + combat.push(blockBadge); + combat.push(entity({ + id: guid('cmb', 216), path: `${PP}/BlockBadge/Value`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 44, parentH: 40, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 44, y: 36 }, pos: { x: 0, y: 0 } }), + sprite({ color: TRANSPARENT }), + text({ value: '0', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + 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 }), + ], + })); + const playerDmgPop = entity({ + id: guid('cmb', 260), path: `${PP}/DmgPop`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 5, + components: [ + transform({ parentW: 300, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 30 }, pos: { x: 16, y: 40 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 22, bold: true, color: { r: 1, g: 0.4, b: 0.35, a: 1 }, alignment: 4 }), + ], + }); + playerDmgPop.jsonString.enable = false; + combat.push(playerDmgPop); + combat.push(entity({ + id: guid('cmb', 200), + path: '/ui/DefaultGroup/CombatHud/TopBar', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 9, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1200, y: 52 }, pos: { x: 0, y: 486 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.06, g: 0.07, b: 0.1, a: 0.82 }, type: 1 }), + ], + })); + const topTexts = [ + ['Floor', -520, 160, '막 1/3', GOLD], + ['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }], + ]; + topTexts.forEach(([suffix, x, w, value, color], ti) => { + combat.push(entity({ + id: guid('cmb', 201 + ti), + path: `/ui/DefaultGroup/CombatHud/TopBar/${suffix}`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: ti, + components: [ + transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 40 }, pos: { x: x, y: 0 } }), + sprite({ color: TRANSPARENT }), + text({ value, fontSize: 22, bold: true, color, alignment: 4 }), + ], + })); + }); + combat.push(entity({ + id: guid('cmb', 209), + path: '/ui/DefaultGroup/CombatHud/TopBar/MesoIcon', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 26, y: 26 }, pos: { x: -432, y: 0 } }), + sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }), + ], + })); + for (let i = 1; i <= 10; i++) { + combat.push(entity({ + id: guid('cmb', 300 + i), + path: `/ui/DefaultGroup/CombatHud/TopBar/RelicSlot${i}`, + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent', + displayOrder: 3 + i, + components: [ + transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: -240 + (i - 1) * 48, y: 0 } }), + sprite({ color: { r: 0.15, g: 0.16, b: 0.2, a: 0.6 }, type: 0, raycast: true }), + { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, + ], + })); + } + combat.push(entity({ + id: guid('cmb', 311), + path: '/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 14, + components: [ + transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 60, y: 30 }, pos: { x: 192, y: 0 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 18, bold: true, color: { r: 0.8, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }), + ], + })); + for (let i = 1; i <= 5; i++) { + combat.push(entity({ + id: guid('cmb', 320 + i), + path: `/ui/DefaultGroup/CombatHud/TopBar/PotionSlot${i}`, + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.UITouchReceiveComponent', + displayOrder: 14 + i, + components: [ + transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 40, y: 40 }, pos: { x: 240 + (i - 1) * 44, y: 0 } }), + sprite({ color: { r: 0.22, g: 0.25, b: 0.3, a: 0.9 }, type: 0, raycast: true }), + { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, + ], + })); + } + const tooltipBox = entity({ + id: guid('cmb', 330), + path: '/ui/DefaultGroup/CombatHud/TooltipBox', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 20, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 150 }, pos: { x: 0, y: 400 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.04, g: 0.05, b: 0.08, a: 0.96 }, type: 1 }), + ], + }); + tooltipBox.jsonString.enable = false; + combat.push(tooltipBox); + combat.push(entity({ + id: guid('cmb', 331), + path: '/ui/DefaultGroup/CombatHud/TooltipBox/Name', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 28 }, pos: { x: 0, y: 52 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 19, bold: true, color: GOLD, alignment: 4 }), + ], + })); + combat.push(entity({ + id: guid('cmb', 332), + path: '/ui/DefaultGroup/CombatHud/TooltipBox/Desc', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 360, parentH: 150, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 332, y: 102 }, pos: { x: 0, y: -18 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 15, bold: false, color: { r: 0.92, g: 0.92, b: 0.95, a: 1 }, alignment: 0 }), + ], + })); + const discardPrompt = entity({ + id: guid('cmb', 333), + path: '/ui/DefaultGroup/CombatHud/DiscardPrompt', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 22, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 520, y: 48 }, pos: { x: 0, y: -260 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.86 }, type: 1 }), + text({ value: '', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), + ], + }); + discardPrompt.jsonString.enable = false; + combat.push(discardPrompt); + const potionMenu = entity({ + id: guid('cmb', 340), + path: '/ui/DefaultGroup/CombatHud/PotionMenu', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 21, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 180 }, pos: { x: 0, y: 120 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.07, g: 0.08, b: 0.12, a: 0.97 }, type: 1 }), + ], + }); + potionMenu.jsonString.enable = false; + combat.push(potionMenu); + combat.push(entity({ + id: guid('cmb', 341), + path: '/ui/DefaultGroup/CombatHud/PotionMenu/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 36 }, pos: { x: 0, y: 52 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 19, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + const pmButtons = [ + ['Use', '사용', -120, { r: 0.32, g: 0.55, b: 0.36, a: 1 }], + ['Toss', '버리기', 0, { r: 0.6, g: 0.32, b: 0.3, a: 1 }], + ['Close', '닫기', 120, { r: 0.25, g: 0.28, b: 0.35, a: 1 }], + ]; + pmButtons.forEach(([suffix, label, x, color], bi) => { + combat.push(entity({ + id: guid('cmb', 342 + bi), + path: `/ui/DefaultGroup/CombatHud/PotionMenu/${suffix}`, + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 1 + bi, + components: [ + transform({ parentW: 380, parentH: 180, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 104, y: 46 }, pos: { x, y: -40 } }), + sprite({ color, type: 1, raycast: true }), + button(), + text({ value: label, fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + }); + combat.push(entity({ + id: guid('cmb', 205), + path: '/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton', + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 3, + components: [ + transform({ parentW: 1200, parentH: 52, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 140, y: 40 }, pos: { x: 528, y: 0 } }), + sprite({ color: DARK, type: 1, raycast: true }), + button(), + text({ value: '모든덱보기', fontSize: 18, bold: true, color: GOLD, alignment: 0 }), + ], + })); + const skillFx = entity({ + id: guid('cmb', 230), path: '/ui/DefaultGroup/CombatHud/SkillFx', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 30, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 110, y: 110 }, pos: { x: 0, y: 0 } }), + sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), + ], + }); + skillFx.jsonString.enable = false; + combat.push(skillFx); + const result = entity({ + id: guid('cmb', 2), + path: '/ui/DefaultGroup/CombatHud/Result', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 8, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }), + ], + }); + result.jsonString.enable = false; + combat.push(result); + return combat; +} diff --git a/tools/deck/hud/deckall.mjs b/tools/deck/hud/deckall.mjs new file mode 100644 index 0000000..34ff6c7 --- /dev/null +++ b/tools/deck/hud/deckall.mjs @@ -0,0 +1,159 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildDeckAll() { + const allDeck = []; + const allHud = entity({ + id: guid('all', 0), + path: '/ui/DefaultGroup/DeckAllHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 16, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }), + ], + }); + allHud.jsonString.enable = false; + allDeck.push(allHud); + allDeck.push(entity({ + id: guid('all', 1), + path: '/ui/DefaultGroup/DeckAllHud/Panel', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1080, y: 800 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }), + ], + })); + allDeck.push(entity({ + id: guid('all', 2), + path: '/ui/DefaultGroup/DeckAllHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 54 }, pos: { x: 0, y: 380 } }), + sprite({ color: TRANSPARENT }), + text({ value: '모든 덱', fontSize: 34, bold: true, color: GOLD, alignment: 4 }), + ], + })); + allDeck.push(entity({ + id: guid('all', 3), + path: '/ui/DefaultGroup/DeckAllHud/Close', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 486, y: 380 } }), + sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), + ], + })); + const deckTabs = [ + { key: 'Warrior', label: '전사', x: -210 }, + { key: 'Thief', label: '도적', x: 0 }, + { key: 'Mage', label: '마법사', x: 210 }, + ]; + for (let i = 0; i < deckTabs.length; i++) { + const tab = deckTabs[i]; + allDeck.push(entity({ + id: guid('all', 10 + i), + path: `/ui/DefaultGroup/DeckAllHud/${tab.key}Tab`, + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 3 + i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 170, y: 46 }, pos: { x: tab.x, y: 318 } }), + sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: tab.label, fontSize: 22, bold: true, color: GOLD, alignment: 4 }), + ], + })); + } + allDeck.push(entity({ + id: guid('all', 4), + path: '/ui/DefaultGroup/DeckAllHud/Empty', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 3, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 40 } }), + sprite({ color: TRANSPARENT }), + text({ value: '덱이 없습니다', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }), + ], + })); + allDeck.push(entity({ + id: guid('all', 5), + path: '/ui/DefaultGroup/DeckAllHud/Grid', + modelId: 'uiempty', + entryId: 'UIEmpty', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent', + displayOrder: 4, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 980, y: 620 }, pos: { x: 0, y: 0 } }), + sprite({ color: TRANSPARENT, type: 1, raycast: true }), + scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }), + ], + })); + const ALL_DECK_CARD_COUNT = 120; + const ALL_DECK_CARD_W = 158; + const ALL_DECK_CARD_H = 214; + // 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조 + for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) { + const allBase = 6 + (i - 1) * 7; + const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`; + const card = entity({ + id: guid('all2', allBase), + path: cardPath, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: i, + components: [ + transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }), + sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }), + ], + }); + card.jsonString.enable = false; + allDeck.push(card); + const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W); + for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) { + const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; + allDeck.push(entity({ + id: guid('all2', allBase + 1 + tIdx), + path: `${cardPath}/${suffix}`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: dOrder, + components: [ + transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), + sprite({ color: TRANSPARENT }), + text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), + ], + })); + } + allDeck.push(entity({ + id: guid('all2', allBase + 6), + path: `${cardPath}/Art`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 5, + components: [ + transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }), + sprite({ color: WHITE, type: 0, raycast: false }), + ], + })); + } + return allDeck; +} diff --git a/tools/deck/hud/deckhud.mjs b/tools/deck/hud/deckhud.mjs new file mode 100644 index 0000000..ca69dae --- /dev/null +++ b/tools/deck/hud/deckhud.mjs @@ -0,0 +1,121 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildDeckHud() { + const hud = []; + const add = (e) => hud.push(e); + + add(entity({ + id: guid('hud', 0), + path: '/ui/DefaultGroup/DeckHud', + modelId: 'uiempty', + entryId: 'UIEmpty', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 5, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }), + sprite({ color: TRANSPARENT }), + ], + })); + + for (const pile of [ + { key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.20, b: 0.25, a: 1 } }, + { key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } }, + ]) { + add(entity({ + id: guid('hud', hud.length), + path: `/ui/DefaultGroup/DeckHud/${pile.key}`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: pile.key === 'DrawPile' ? 0 : 1, + components: [ + transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }), + sprite({ color: pile.color, type: 1, raycast: true }), + button(), + ], + })); + add(entity({ + id: guid('hud', hud.length), + path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }), + sprite({ color: TRANSPARENT }), + text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }), + ], + })); + add(entity({ + id: guid('hud', hud.length), + path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }), + sprite({ color: TRANSPARENT }), + text({ value: pile.count, fontSize: 42, bold: true }), + ], + })); + } + + add(entity({ + id: guid('hud', hud.length), + path: '/ui/DefaultGroup/DeckHud/EndTurnButton', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 64 }, pos: { x: 560, y: 160 }, align: ALIGN_CENTER }), + sprite({ color: DARK, type: 1, raycast: true }), + button(), + text({ value: '턴 종료', fontSize: 28, bold: true, color: GOLD, alignment: 0 }), + ], + })); + + add(entity({ + id: guid('hud', hud.length), + path: '/ui/DefaultGroup/DeckHud/EnergyOrb', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 3, + components: [ + transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 96, y: 96 }, pos: { x: -560, y: 160 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.12, g: 0.2, b: 0.34, a: 0.95 }, type: 1 }), + ], + })); + add(entity({ + id: guid('hud', hud.length), + path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Value', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 48 }, pos: { x: 0, y: 6 } }), + sprite({ color: TRANSPARENT }), + text({ value: '3/3', fontSize: 34, bold: true, color: { r: 0.65, g: 0.92, b: 1, a: 1 }, alignment: 4 }), + ], + })); + add(entity({ + id: guid('hud', hud.length), + path: '/ui/DefaultGroup/DeckHud/EnergyOrb/Label', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 96, parentH: 96, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 92, y: 24 }, pos: { x: 0, y: -28 } }), + sprite({ color: TRANSPARENT }), + text({ value: '에너지', fontSize: 14, bold: true, color: { r: 0.55, g: 0.7, b: 0.85, a: 1 }, alignment: 4 }), + ], + })); + + return hud; +} diff --git a/tools/deck/hud/deckinspect.mjs b/tools/deck/hud/deckinspect.mjs new file mode 100644 index 0000000..d077e90 --- /dev/null +++ b/tools/deck/hud/deckinspect.mjs @@ -0,0 +1,138 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildDeckInspect() { + const inspect = []; + const inspectHud = entity({ + id: guid('ins', 0), + path: '/ui/DefaultGroup/DeckInspectHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 15, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.78 }, type: 1, raycast: true }), + ], + }); + inspectHud.jsonString.enable = false; + inspect.push(inspectHud); + inspect.push(entity({ + id: guid('ins', 1), + path: '/ui/DefaultGroup/DeckInspectHud/Panel', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1040, y: 760 }, pos: { x: 0, y: 10 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.08, g: 0.09, b: 0.11, a: 0.96 }, type: 1 }), + ], + })); + inspect.push(entity({ + id: guid('ins', 2), + path: '/ui/DefaultGroup/DeckInspectHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 54 }, pos: { x: 0, y: 350 } }), + sprite({ color: TRANSPARENT }), + text({ value: '\uB371 \uBCF4\uAE30', fontSize: 34, bold: true, color: GOLD, alignment: 4 }), + ], + })); + inspect.push(entity({ + id: guid('ins', 3), + path: '/ui/DefaultGroup/DeckInspectHud/Close', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 78, y: 52 }, pos: { x: 466, y: 350 } }), + sprite({ color: { r: 0.16, g: 0.18, b: 0.22, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: 'X', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), + ], + })); + inspect.push(entity({ + id: guid('ins', 4), + path: '/ui/DefaultGroup/DeckInspectHud/Empty', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 3, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }), + sprite({ color: TRANSPARENT }), + text({ value: '\uCE74\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4', fontSize: 28, bold: true, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 4 }), + ], + })); + inspect.push(entity({ + id: guid('ins', 5), + path: '/ui/DefaultGroup/DeckInspectHud/Grid', + modelId: 'uiempty', + entryId: 'UIEmpty', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent', + displayOrder: 4, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 950, y: 610 }, pos: { x: 0, y: 0 } }), + sprite({ color: TRANSPARENT, type: 1, raycast: true }), + scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }), + ], + })); + const INSPECT_CARD_COUNT = 60; + const INSPECT_CARD_W = 158; + const INSPECT_CARD_H = 214; + // 카드 단위 엔티티는 v2 네임스페이스(ins2/all2/rwd2/shp2) — 자식 구성이 바뀌면 id를 통째로 새로 발급해야 함. + // 구 id를 다른 path에 재사용하면 메이커 refresh의 id 기준 in-place 병합이 꼬여 자식이 소실됨 (P13 실측). + for (let i = 1; i <= INSPECT_CARD_COUNT; i++) { + const insBase = 6 + (i - 1) * 7; + const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`; + const card = entity({ + id: guid('ins2', insBase), + path: cardPath, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: i, + components: [ + transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }), + sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }), + ], + }); + card.jsonString.enable = false; + inspect.push(card); + const inspectLayout = cardFaceLayout(INSPECT_CARD_W); + for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) { + const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; + inspect.push(entity({ + id: guid('ins2', insBase + 1 + tIdx), + path: `${cardPath}/${suffix}`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: dOrder, + components: [ + transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), + sprite({ color: TRANSPARENT }), + text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), + ], + })); + } + inspect.push(entity({ + id: guid('ins2', insBase + 6), + path: `${cardPath}/Art`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 5, + components: [ + transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }), + sprite({ color: WHITE, type: 0, raycast: false }), + ], + })); + } + return inspect; +} diff --git a/tools/deck/hud/jobchoice.mjs b/tools/deck/hud/jobchoice.mjs new file mode 100644 index 0000000..de336db --- /dev/null +++ b/tools/deck/hud/jobchoice.mjs @@ -0,0 +1,51 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildJobChoice() { + const jobChoice = []; + const jobChoiceHud = entity({ + id: guid('job', 0), + path: '/ui/DefaultGroup/JobChoiceHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 9, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), + ], + }); + jobChoiceHud.jsonString.enable = false; + jobChoice.push(jobChoiceHud); + jobChoice.push(entity({ + id: guid('job', 1), + path: '/ui/DefaultGroup/JobChoiceHud/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 220 } }), + sprite({ color: TRANSPARENT }), + text({ value: '보스 처치 보상을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), + ], + })); + const jcButtons = [ + ['RelicButton', '유물 획득', -240, { r: 0.7, g: 0.55, b: 0.85, a: 1 }], + ['JobButton', '2차 전직', 240, { r: 0.86, g: 0.6, b: 0.3, a: 1 }], + ]; + jcButtons.forEach(([suffix, label, x, color], bi) => { + jobChoice.push(entity({ + id: guid('job', 2 + bi), + path: `/ui/DefaultGroup/JobChoiceHud/${suffix}`, + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 1 + bi, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 140 }, pos: { x, y: 0 } }), + sprite({ color, type: 1, raycast: true }), + button(), + text({ value: label, fontSize: 32, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + }); + return jobChoice; +} diff --git a/tools/deck/hud/jobselect.mjs b/tools/deck/hud/jobselect.mjs new file mode 100644 index 0000000..494d08d --- /dev/null +++ b/tools/deck/hud/jobselect.mjs @@ -0,0 +1,89 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildJobSelect() { + const jobSelect = []; + const jobSelectHud = entity({ + id: guid('job', 10), + path: '/ui/DefaultGroup/JobSelectHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 10, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.94 }, type: 1, raycast: true }), + ], + }); + jobSelectHud.jsonString.enable = false; + jobSelect.push(jobSelectHud); + jobSelect.push(entity({ + id: guid('job', 11), + path: '/ui/DefaultGroup/JobSelectHud/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 300 } }), + sprite({ color: TRANSPARENT }), + text({ value: '2차 전직 — 직업을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), + ], + })); + // 범용 슬롯 3개 — ShowJobSelect(Lua)가 클래스별 JOBS로 텍스트를 채움 (P10 동적화) + const jobs = [ + ['slot1', '', '', '', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }], + ['slot2', '', '', '', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }], + ['slot3', '', '', '', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }], + ]; + jobs.forEach(([jobId, name, desc, starter, x, color], ji) => { + const base = `/ui/DefaultGroup/JobSelectHud/Job_${jobId}`; + jobSelect.push(entity({ + id: guid('job', 12 + ji * 4), + path: base, + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 1 + ji, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 420 }, pos: { x, y: -20 } }), + sprite({ color, type: 1, raycast: true }), + button(), + ], + })); + jobSelect.push(entity({ + id: guid('job', 13 + ji * 4), + path: `${base}/Name`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 50 }, pos: { x: 0, y: 150 } }), + sprite({ color: TRANSPARENT }), + text({ value: name, fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + jobSelect.push(entity({ + id: guid('job', 14 + ji * 4), + path: `${base}/Desc`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 160 }, pos: { x: 0, y: 0 } }), + sprite({ color: TRANSPARENT }), + text({ value: desc, fontSize: 22, bold: false, color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, alignment: 4 }), + ], + })); + jobSelect.push(entity({ + id: guid('job', 15 + ji * 4), + path: `${base}/Starter`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 32 }, pos: { x: 0, y: -160 } }), + sprite({ color: TRANSPARENT }), + text({ value: starter, fontSize: 18, bold: true, color: GOLD, alignment: 4 }), + ], + })); + }); + return jobSelect; +} diff --git a/tools/deck/hud/lobby.mjs b/tools/deck/hud/lobby.mjs new file mode 100644 index 0000000..65b1ba0 --- /dev/null +++ b/tools/deck/hud/lobby.mjs @@ -0,0 +1,58 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildLobby() { + const lobby = []; + let lobId = 0; + const lobbyRoot = entity({ + id: guid('lob', lobId++), + path: '/ui/DefaultGroup/LobbyHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 11, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + // 로비가 물리 맵이 됨 — 투명 + 비레이캐스트로 맵을 가리지 않고 월드 NPC 클릭이 통과되게 함. + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + ], + }); + lobbyRoot.jsonString.enable = false; + lobby.push(lobbyRoot); + const lobTexts = [ + ['Title', 0, 478, 760, '메이플 로비', 40, GOLD], + ['SoulLabel', 700, 478, 320, '영혼 0', 28, { r: 0.6, g: 0.85, b: 1, a: 1 }], + ['AscLabel', -560, 478, 380, '승천 0 / 해금 0', 22, { r: 0.9, g: 0.7, b: 0.5, a: 1 }], + ['Hint', 0, -478, 1500, 'NPC에게 다가가 ↑ 또는 클릭으로 대화 · ← → 이동 · Ctrl 공격', 20, { r: 0.72, g: 0.76, b: 0.82, a: 1 }], + ]; + for (const [suffix, x, y, w, value, fs, color] of lobTexts) { + lobby.push(entity({ + id: guid('lob', lobId++), + path: `/ui/DefaultGroup/LobbyHud/${suffix}`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 56 }, pos: { x, y } }), + sprite({ color: TRANSPARENT }), + text({ value, fontSize: fs, bold: true, color, alignment: 4 }), + ], + })); + } + for (const [suffix, x, label] of [['AscMinus', -780, '<'], ['AscPlus', -540, '>']]) { + lobby.push(entity({ + id: guid('lob', lobId++), + path: `/ui/DefaultGroup/LobbyHud/${suffix}`, + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 46, y: 42 }, pos: { x, y: 470 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: label, fontSize: 28, bold: true, color: WHITE, alignment: 4 }), + ], + })); + } + // NPC 4종은 로비 물리 맵의 월드 엔티티(map/lobby.map + LobbyNpc codeblock)로 이동. UI 버튼 행 제거. + return lobby; +} diff --git a/tools/deck/hud/map.mjs b/tools/deck/hud/map.mjs new file mode 100644 index 0000000..87a229a --- /dev/null +++ b/tools/deck/hud/map.mjs @@ -0,0 +1,162 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildMap() { + const TYPE_KO = { combat: '전투', elite: '엘리트', boss: '보스', shop: '상점', rest: '휴식' }; + const map = []; + const mapHud = entity({ + id: guid('map', 0), + path: '/ui/DefaultGroup/MapHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 7, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + // 불투명 다크 배경(게임 월드 가림 보장). 그 위 BgImage 자식이 배경 스프라이트를 얹는다. + sprite({ color: { r: 0.06, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }), + ], + }); + mapHud.jsonString.enable = false; + map.push(mapHud); + // 배경 이미지(displayOrder 0 = 도트/타이틀/노드 아래). nodeicons.json background는 SPRITE RUID여야 렌더됨 + // — 메이플 BackgroundComponent 리소스는 UI 스프라이트로 안 뜬다. 유효 스프라이트면 풀스크린 표시, 아니면 투명(다크 배경 노출). + map.push(entity({ + id: guid('map', 990), + path: '/ui/DefaultGroup/MapHud/BgImage', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ dataId: NODEICONS.background, color: { r: 0.5, g: 0.52, b: 0.58, a: 1 }, type: 0, raycast: false }), + ], + })); + map.push(entity({ + id: guid('map', 1), + path: '/ui/DefaultGroup/MapHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 510 } }), + sprite({ color: TRANSPARENT }), + text({ value: '다음 노드 선택', fontSize: 40, bold: true, color: GOLD, alignment: 4 }), + ], + })); + // 절차 생성 맵용 정적 그리드 — 가로 진행(왼→오른쪽): 행(row)=x축, 열(col)=y축 분기, 보스는 최우측 중앙. + const nodeX = (row) => -540 + (row - 1) * 150; + const nodeY = (col) => 180 - (col - 1) * 120; + const BOSS_POS = { x: nodeX(MAP_ROWS) + 150, y: 0 }; + let mapN = 2; + const pushMapNode = (id, pos, size, label) => { + const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`; + const nodeEnt = entity({ + id: guid('map', mapN++), + path: nodePath, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 5, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }), + sprite({ color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }), + button(), + ], + }); + nodeEnt.jsonString.enable = false; + map.push(nodeEnt); + }; + for (let r = 1; r <= MAP_ROWS; r++) { + for (let c = 1; c <= MAP_COLS; c++) { + pushMapNode(`r${r}c${c}`, { x: nodeX(r), y: nodeY(c) }, { x: 64, y: 64 }, ''); + } + } + pushMapNode('boss', BOSS_POS, { x: 88, y: 88 }, '보스'); + const pushDots = (dotId, from, to) => { + for (let k = 1; k <= 3; k++) { + const t = k / 4; + const dot = entity({ + id: guid('map', mapN++), + path: `/ui/DefaultGroup/MapHud/Dot_${dotId}_${k}`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 8, y: 8 }, pos: { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t } }), + sprite({ color: { r: 0.5, g: 0.5, b: 0.55, a: 0.8 }, type: 1 }), + ], + }); + dot.jsonString.enable = false; + map.push(dot); + } + }; + for (let r = 1; r < MAP_ROWS; r++) { + for (let c = 1; c <= MAP_COLS; c++) { + for (let c2 = c - 1; c2 <= c + 1; c2++) { + if (c2 < 1 || c2 > MAP_COLS) continue; + pushDots(`r${r}c${c}_${c2}`, { x: nodeX(r), y: nodeY(c) }, { x: nodeX(r + 1), y: nodeY(c2) }); + } + } + } + for (let c = 1; c <= MAP_COLS; c++) { + pushDots(`r${MAP_ROWS}c${c}_b`, { x: nodeX(MAP_ROWS), y: nodeY(c) }, BOSS_POS); + } + // 노드 종류 범례 (우측 하단) — 각 타입 아이콘 + 이름 + const LEGEND = [['combat', '전투'], ['elite', '엘리트'], ['boss', '보스'], ['shop', '상점'], ['rest', '휴식'], ['treasure', '보물']]; + const lgW = 300, lgH = 312; + map.push(entity({ + id: guid('map', 991), + path: '/ui/DefaultGroup/MapHud/Legend', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 4, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: lgW, y: lgH }, pos: { x: 760, y: -334 } }), + sprite({ color: { r: 0.08, g: 0.09, b: 0.14, a: 0.86 }, type: 1, raycast: false }), + ], + })); + map.push(entity({ + id: guid('map', 992), + path: '/ui/DefaultGroup/MapHud/Legend/LegendTitle', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: lgW, parentH: lgH, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: lgW - 20, y: 32 }, pos: { x: 0, y: lgH / 2 - 26 } }), + sprite({ color: TRANSPARENT }), + text({ value: '노드 종류', fontSize: 22, bold: true, color: GOLD, alignment: 4 }), + ], + })); + let lgId = 993; + LEGEND.forEach(([t, ko], i) => { + const rowY = lgH / 2 - 78 - i * 38; + map.push(entity({ + id: guid('map', lgId++), + path: `/ui/DefaultGroup/MapHud/Legend/Icon_${t}`, + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 2, + components: [ + transform({ parentW: lgW, parentH: lgH, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 36, y: 36 }, pos: { x: -lgW / 2 + 38, y: rowY } }), + sprite({ dataId: NODEICONS.icons[t], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), + ], + })); + map.push(entity({ + id: guid('map', lgId++), + path: `/ui/DefaultGroup/MapHud/Legend/Label_${t}`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: lgW, parentH: lgH, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: lgW - 110, y: 30 }, pos: { x: 32, y: rowY } }), + sprite({ color: TRANSPARENT }), + text({ value: ko, fontSize: 19, bold: false, color: { r: 0.9, g: 0.92, b: 0.96, a: 1 }, alignment: 4 }), + ], + })); + }); + return map; +} diff --git a/tools/deck/hud/rest.mjs b/tools/deck/hud/rest.mjs new file mode 100644 index 0000000..fb9d6f3 --- /dev/null +++ b/tools/deck/hud/rest.mjs @@ -0,0 +1,61 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildRest() { + const rest = []; + const restHud = entity({ + id: guid('rst', 0), + path: '/ui/DefaultGroup/RestHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 9, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.08, b: 0.06, a: 0.92 }, type: 1, raycast: true }), + ], + }); + restHud.jsonString.enable = false; + rest.push(restHud); + rest.push(entity({ + id: guid('rst', 1), + path: '/ui/DefaultGroup/RestHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 140 } }), + sprite({ color: TRANSPARENT }), + text({ value: '휴식', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), + ], + })); + rest.push(entity({ + id: guid('rst', 2), + path: '/ui/DefaultGroup/RestHud/Info', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 600, y: 50 }, pos: { x: 0, y: 30 } }), + sprite({ color: TRANSPARENT }), + text({ value: 'HP 회복', fontSize: 30, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + rest.push(entity({ + id: guid('rst', 3), + path: '/ui/DefaultGroup/RestHud/Leave', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -120 } }), + sprite({ color: DARK, type: 1, raycast: true }), + button(), + text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), + ], + })); + return rest; +} diff --git a/tools/deck/hud/reward.mjs b/tools/deck/hud/reward.mjs new file mode 100644 index 0000000..6105513 --- /dev/null +++ b/tools/deck/hud/reward.mjs @@ -0,0 +1,98 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildReward() { + const reward = []; + const rewardHud = entity({ + id: guid('rwd', 0), + path: '/ui/DefaultGroup/RewardHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 6, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.86 }, type: 1, raycast: true }), + ], + }); + rewardHud.jsonString.enable = false; + reward.push(rewardHud); + reward.push(entity({ + id: guid('rwd', 1), + path: '/ui/DefaultGroup/RewardHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 64 }, pos: { x: 0, y: 300 } }), + sprite({ color: TRANSPARENT }), + text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), + ], + })); + const rewardXs = [-300, 0, 300]; + // 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조 + for (let i = 1; i <= 3; i++) { + const rwdBase = 2 + (i - 1) * 7; + const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`; + reward.push(entity({ + id: guid('rwd2', rwdBase), + path: cardPath, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent', + displayOrder: i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }), + sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }), + button(), + { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, + ], + })); + const rewardLayout = cardFaceLayout(CARD_W); + for (const [tIdx, [suffix, cfg]] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]).entries()) { + const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8; + reward.push(entity({ + id: guid('rwd2', rwdBase + 1 + tIdx), + path: `${cardPath}/${suffix}`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: dOrder, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), + sprite({ color: TRANSPARENT }), + text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), + ], + })); + } + reward.push(entity({ + id: guid('rwd2', rwdBase + 6), + path: `${cardPath}/Art`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 5, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: rewardLayout.art.size, pos: rewardLayout.art.pos }), + sprite({ color: WHITE, type: 0, raycast: false }), + ], + })); + } + let rwdN = 2 + 3 * 7; // 구 시퀀스의 루프 종료 시점 값(23) 보존 — Skip 등 후속 id 불변 + reward.push(entity({ + id: guid('rwd', rwdN++), + path: '/ui/DefaultGroup/RewardHud/Skip', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 10, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -260 } }), + sprite({ color: DARK, type: 1, raycast: true }), + button(), + text({ value: '건너뛰기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), + ], + })); + return reward; +} diff --git a/tools/deck/hud/shop.mjs b/tools/deck/hud/shop.mjs new file mode 100644 index 0000000..d0166d8 --- /dev/null +++ b/tools/deck/hud/shop.mjs @@ -0,0 +1,203 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildShop() { + const shop = []; + const shopHud = entity({ + id: guid('shp', 0), + path: '/ui/DefaultGroup/ShopHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 8, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), + ], + }); + shopHud.jsonString.enable = false; + shop.push(shopHud); + shop.push(entity({ + id: guid('shp', 1), + path: '/ui/DefaultGroup/ShopHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 400 } }), + sprite({ color: TRANSPARENT }), + text({ value: '상점', fontSize: 44, bold: true, color: GOLD, alignment: 4 }), + ], + })); + shop.push(entity({ + id: guid('shp', 2), + path: '/ui/DefaultGroup/ShopHud/Gold', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }), + sprite({ color: TRANSPARENT }), + text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), + ], + })); + shop.push(entity({ + id: guid('shp', 3), + path: '/ui/DefaultGroup/ShopHud/MesoIcon', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 30, y: 30 }, pos: { x: -86, y: 330 } }), + sprite({ color: { r: 1, g: 0.82, b: 0.2, a: 1 }, type: 1 }), + ], + })); + const shopXs = [-300, 0, 300]; + // 카드 단위 엔티티 v2 네임스페이스 (stride 8: Price 포함) — DeckInspectHud 주석 참조 + for (let i = 1; i <= 3; i++) { + const shpBase = 3 + (i - 1) * 8; + const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`; + shop.push(entity({ + id: guid('shp2', shpBase), + path: cardPath, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.UITouchReceiveComponent', + displayOrder: i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }), + sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }), + button(), + { '@type': 'MOD.Core.UITouchReceiveComponent', Enable: true }, + ], + })); + const shopLayout = cardFaceLayout(CARD_W); + const shopTexts = shopLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]); + shopTexts.push(['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -135 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }]); + for (const [tIdx, [suffix, cfg]] of shopTexts.entries()) { + const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9; + shop.push(entity({ + id: guid('shp2', shpBase + 1 + tIdx), + path: `${cardPath}/${suffix}`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: dOrder, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), + sprite({ color: TRANSPARENT }), + text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color, dropShadow: cfg.dropShadow, outlineWidth: cfg.outlineWidth }), + ], + })); + } + shop.push(entity({ + id: guid('shp2', shpBase + 7), + path: `${cardPath}/Art`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 5, + components: [ + transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: shopLayout.art.size, pos: shopLayout.art.pos }), + sprite({ color: WHITE, type: 0, raycast: false }), + ], + })); + } + let shpN = 3 + 3 * 8; // 구 시퀀스의 루프 종료 시점 값(27) 보존 — Relic 등 후속 id 불변 + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Relic', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 9, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -190 } }), + sprite({ color: { r: 0.7, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }), + button(), + ], + })); + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Relic/Label', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }), + sprite({ color: TRANSPARENT }), + text({ value: '유물', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Relic/Price', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }), + sprite({ color: TRANSPARENT }), + text({ value: '60 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), + ], + })); + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Potion', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 11, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 560, y: 76 }, pos: { x: 0, y: -278 } }), + sprite({ color: { r: 0.45, g: 0.7, b: 0.55, a: 1 }, type: 1, raycast: true }), + button(), + ], + })); + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Potion/Label', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 40 }, pos: { x: 0, y: 12 } }), + sprite({ color: TRANSPARENT }), + text({ value: '물약', fontSize: 22, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Potion/Price', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }), + sprite({ color: TRANSPARENT }), + text({ value: '20 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), + ], + })); + shop.push(entity({ + id: guid('shp', shpN++), + path: '/ui/DefaultGroup/ShopHud/Leave', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 10, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -380 } }), + sprite({ color: DARK, type: 1, raycast: true }), + button(), + text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), + ], + })); + return shop; +} diff --git a/tools/deck/hud/soulshop.mjs b/tools/deck/hud/soulshop.mjs new file mode 100644 index 0000000..2e45c59 --- /dev/null +++ b/tools/deck/hud/soulshop.mjs @@ -0,0 +1,90 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildSoulShop() { + const soulShop = []; + let soulId = 0; + const soulRoot = entity({ + id: guid('soul', soulId++), + path: '/ui/DefaultGroup/SoulShopHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 15, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.95 }, type: 1, raycast: true }), + ], + }); + soulRoot.jsonString.enable = false; + soulShop.push(soulRoot); + soulShop.push(entity({ + id: guid('soul', soulId++), + path: '/ui/DefaultGroup/SoulShopHud/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 410 } }), + sprite({ color: TRANSPARENT }), + text({ value: '영혼 상점', fontSize: 44, bold: true, color: { r: 0.6, g: 0.85, b: 1, a: 1 }, alignment: 4 }), + ], + })); + soulShop.push(entity({ + id: guid('soul', soulId++), + path: '/ui/DefaultGroup/SoulShopHud/Souls', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 400, y: 44 }, pos: { x: 0, y: 345 } }), + sprite({ color: TRANSPARENT }), + text({ value: '영혼 0', fontSize: 28, bold: true, color: { r: 0.6, g: 0.85, b: 1, a: 1 }, alignment: 4 }), + ], + })); + soulShop.push(entity({ + id: guid('soul', soulId++), + path: '/ui/DefaultGroup/SoulShopHud/Close', + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -400 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.2, g: 0.24, b: 0.3, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }), + ], + })); + for (let i = 1; i <= 4; i++) { + const ip = `/ui/DefaultGroup/SoulShopHud/Item${i}`; + const iy = 230 - (i - 1) * 125; + soulShop.push(entity({ + id: guid('soul', soulId++), + path: ip, modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 104 }, pos: { x: 0, y: iy }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.14, g: 0.16, b: 0.22, a: 1 }, type: 1, raycast: true }), + button(), + ], + })); + for (const [suffix, x, y, w, fs, color] of [ + ['Name', -180, 22, 360, 28, GOLD], + ['Desc', -180, -24, 440, 20, { r: 0.86, g: 0.9, b: 0.94, a: 1 }], + ['Status', 270, 0, 220, 22, { r: 0.6, g: 0.85, b: 1, a: 1 }], + ]) { + soulShop.push(entity({ + id: guid('soul', soulId++), + path: `${ip}/${suffix}`, modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 760, parentH: 104, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 38 }, pos: { x, y } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: fs, bold: suffix === 'Name', color, alignment: 4 }), + ], + })); + } + } + return soulShop; +} diff --git a/tools/deck/hud/treasure.mjs b/tools/deck/hud/treasure.mjs new file mode 100644 index 0000000..8fecfaa --- /dev/null +++ b/tools/deck/hud/treasure.mjs @@ -0,0 +1,89 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildTreasure() { + const treasure = []; + const treasureHud = entity({ + id: guid('trs', 0), + path: '/ui/DefaultGroup/TreasureHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 8, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), + ], + }); + treasureHud.jsonString.enable = false; + treasure.push(treasureHud); + treasure.push(entity({ + id: guid('trs', 1), + path: '/ui/DefaultGroup/TreasureHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 60 }, pos: { x: 0, y: 320 } }), + sprite({ color: TRANSPARENT }), + text({ value: '보물 상자', fontSize: 40, bold: true, color: GOLD, alignment: 4 }), + ], + })); + treasure.push(entity({ + id: guid('trs', 2), + path: '/ui/DefaultGroup/TreasureHud/Chest', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 180 }, pos: { x: 0, y: 40 } }), + sprite({ dataId: CHEST_CLOSED_RUID, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }), + button(), + ], + })); + treasure.push(entity({ + id: guid('trs', 3), + path: '/ui/DefaultGroup/TreasureHud/Hint', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 500, y: 34 }, pos: { x: 0, y: -90 } }), + sprite({ color: TRANSPARENT }), + text({ value: '상자를 클릭해 여세요', fontSize: 20, bold: false, color: { r: 0.85, g: 0.85, b: 0.9, a: 1 }, alignment: 4 }), + ], + })); + const treasureReward = entity({ + id: guid('trs', 4), + path: '/ui/DefaultGroup/TreasureHud/Reward', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 3, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 44 }, pos: { x: 0, y: -160 } }), + sprite({ color: TRANSPARENT }), + text({ value: '', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }), + ], + }); + treasureReward.jsonString.enable = false; + treasure.push(treasureReward); + treasure.push(entity({ + id: guid('trs', 5), + path: '/ui/DefaultGroup/TreasureHud/Leave', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 4, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -280 } }), + sprite({ color: DARK, type: 1, raycast: true }), + button(), + text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }), + ], + })); + return treasure; +} -- 2.49.1 From eafd6747a7a1471dd478f9d8e38d06fc275fb6c6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:40:47 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor(gen):=20MainMenu=C2=B7CharacterSel?= =?UTF-8?q?ectHud=EB=A5=BC=20hud/*.mjs=EB=A1=9C=20=EC=B6=94=EC=B6=9C=20(?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emit이 묶여있던 menu/select 쌍을 buildMainMenu/buildCharSelect로 분리 (select[0].enable=false는 charselect에 포함). HUD 16종 모듈화 완료. 산출물 무변경(diffcheck IDENTICAL). Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/deck/gen-slaydeck.mjs | 306 +--------------------------------- tools/deck/hud/charselect.mjs | 185 ++++++++++++++++++++ tools/deck/hud/mainmenu.mjs | 127 ++++++++++++++ 3 files changed, 316 insertions(+), 302 deletions(-) create mode 100644 tools/deck/hud/charselect.mjs create mode 100644 tools/deck/hud/mainmenu.mjs diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 79b1f1f..a976782 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -15,6 +15,8 @@ import { buildJobSelect } from './hud/jobselect.mjs'; import { buildLobby } from './hud/lobby.mjs'; import { buildBoard } from './hud/board.mjs'; import { buildSoulShop } from './hud/soulshop.mjs'; +import { buildMainMenu } from './hud/mainmenu.mjs'; +import { buildCharSelect } from './hud/charselect.mjs'; import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from './lib/ui-helpers.mjs'; function upsertUi() { @@ -205,308 +207,8 @@ function upsertUi() { emit('JobSelectHud', buildJobSelect()); - const menu = []; - menu.push(entity({ - id: guid('menu', 0), - path: '/ui/DefaultGroup/MainMenu', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 20, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.06, g: 0.09, b: 0.13, a: 1 }, type: 1, raycast: true }), - ], - })); - menu.push(entity({ - id: guid('menu', 50), - path: '/ui/DefaultGroup/MainMenu/OpaqueBackdrop', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT, type: 1, raycast: false }), - ], - })); - menu.push(entity({ - id: guid('menu', 1), - path: '/ui/DefaultGroup/MainMenu/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 100 }, pos: { x: 0, y: 180 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT }), - text({ value: '슬레이 메이플', fontSize: 64, bold: true, color: GOLD, alignment: 0 }), - ], - })); - menu.push(entity({ - id: guid('menu', 2), - path: '/ui/DefaultGroup/MainMenu/Subtitle', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 48 }, pos: { x: 0, y: 104 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT }), - text({ value: '카드를 뽑고, 덱을 만들고, 첨탑을 오른다', fontSize: 24, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 0 }), - ], - })); - menu.push(entity({ - id: guid('menu', 3), - path: '/ui/DefaultGroup/MainMenu/NewGameButton', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 68 }, pos: { x: 0, y: -20 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '새 게임', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), - ], - })); - // 승천 선택 (P11): [-] 라벨 [+] - menu.push(entity({ - id: guid('menu', 195), - path: '/ui/DefaultGroup/MainMenu/AscMinus', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 5, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: -170, y: -185 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '-', fontSize: 30, bold: true, color: GOLD, alignment: 4 }), - ], - })); - menu.push(entity({ - id: guid('menu', 196), - path: '/ui/DefaultGroup/MainMenu/AscLabel', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 6, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 250, y: 40 }, pos: { x: 0, y: -185 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT }), - text({ value: '승천 0 / 해금 0', fontSize: 22, bold: true, color: { r: 0.85, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }), - ], - })); - menu.push(entity({ - id: guid('menu', 197), - path: '/ui/DefaultGroup/MainMenu/AscPlus', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 7, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: 170, y: -185 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '+', fontSize: 30, bold: true, color: GOLD, alignment: 4 }), - ], - })); - menu.push(entity({ - id: guid('menu', 4), - path: '/ui/DefaultGroup/MainMenu/ContinueButton', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 3, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 58 }, pos: { x: 0, y: -100 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.1, g: 0.11, b: 0.13, a: 0.78 }, type: 1, raycast: false }), - button({ enabled: false }), - text({ value: '이어하기', fontSize: 24, bold: true, color: { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 0 }), - ], - })); - const select = []; - select.push(entity({ - id: guid('menu', 100), - path: '/ui/DefaultGroup/CharacterSelectHud', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 21, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }), - ], - })); - select.push(entity({ - id: guid('menu', 190), - path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop', - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT, type: 1, raycast: false }), - ], - })); - select.push(entity({ - id: guid('menu', 101), - path: '/ui/DefaultGroup/CharacterSelectHud/Title', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 1, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT }), - text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }), - ], - })); - select.push(entity({ - id: guid('menu', 102), - path: '/ui/DefaultGroup/CharacterSelectHud/Status', - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }), - sprite({ color: TRANSPARENT }), - text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }), - ], - })); - const classCards = [ - { key: 'Warrior', classId: 'warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } }, - { key: 'Thief', classId: 'bandit', label: '\uB3C4\uC801', desc: '\uB3C5\u00B7\uB2E8\uAC80\u00B7\uB4DC\uB85C\uC6B0', x: 0, enabled: true, tint: { r: 0.26, g: 0.5, b: 0.34, a: 1 } }, - { key: 'Mage', classId: 'magician', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } }, - ]; - for (let i = 0; i < classCards.length; i++) { - const cls = classCards[i]; - const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`; - select.push(entity({ - id: guid('menu', 110 + i), - path: base, - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', - displayOrder: 10 + i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }), - sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }), - button({ enabled: cls.enabled }), - ], - })); - select.push(entity({ - id: guid('menu', 200 + i), - path: `${base}/Art`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 0, - components: [ - transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 318 }, pos: { x: 0, y: 0 } }), - sprite({ dataId: CHARS.portraits[cls.classId], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), - ], - })); - select.push(entity({ - id: guid('menu', 210 + i), - path: `${base}/NameBanner`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 1, - components: [ - transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 60 }, pos: { x: 0, y: -137 } }), - sprite({ color: { r: 0, g: 0, b: 0, a: 0.55 }, type: 1, raycast: false }), - ], - })); - select.push(entity({ - id: guid('menu', 120 + i), - path: `${base}/Name`, - modelId: 'uitext', - entryId: 'UIText', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', - displayOrder: 2, - components: [ - transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 54 }, pos: { x: 0, y: -137 } }), - sprite({ color: TRANSPARENT }), - text({ value: cls.label, fontSize: 34, bold: true, color: cls.enabled ? GOLD : { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 4 }), - ], - })); - if (!cls.enabled) { - select.push(entity({ - id: guid('menu', 150 + i), - path: `${base}/LockBody`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 3, - components: [ - transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 76, y: 58 }, pos: { x: 0, y: 4 } }), - sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }), - ], - })); - select.push(entity({ - id: guid('menu', 160 + i), - path: `${base}/LockShackle`, - modelId: 'uisprite', - entryId: 'UISprite', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', - displayOrder: 4, - components: [ - transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 54, y: 42 }, pos: { x: 0, y: 48 } }), - sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }), - ], - })); - } - select.push(entity({ - id: guid('menu', 170 + i), - path: `/ui/DefaultGroup/CharacterSelectHud/${cls.key}DeckButton`, - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 18 + i, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 46 }, pos: { x: cls.x, y: -160 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }), - button({ enabled: cls.enabled }), - text({ value: '\uB371 \uBCF4\uAE30', fontSize: 20, bold: true, color: GOLD, alignment: 0 }), - ], - })); - } - select.push(entity({ - id: guid('menu', 180), - path: '/ui/DefaultGroup/CharacterSelectHud/StartButton', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 20, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), - ], - })); - select.push(entity({ - id: guid('menu', 230), - path: '/ui/DefaultGroup/CharacterSelectHud/BackButton', - modelId: 'uibutton', - entryId: 'UIButton', - componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', - displayOrder: 22, - components: [ - transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 56 }, pos: { x: -800, y: 430 }, align: ALIGN_CENTER }), - sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }), - button(), - text({ value: '\u2190 \uB4A4\uB85C', fontSize: 26, bold: true, color: GOLD, alignment: 0 }), - ], - })); - select[0].jsonString.enable = false; - emit('MainMenu', menu); - emit('CharacterSelectHud', select); + emit('MainMenu', buildMainMenu()); + emit('CharacterSelectHud', buildCharSelect()); // ── LobbyHud — 반복 런의 허브. NPC 클릭으로 런시작/도감/영혼상점/게시판 ── emit('LobbyHud', buildLobby()); diff --git a/tools/deck/hud/charselect.mjs b/tools/deck/hud/charselect.mjs new file mode 100644 index 0000000..5601d22 --- /dev/null +++ b/tools/deck/hud/charselect.mjs @@ -0,0 +1,185 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildCharSelect() { + const select = []; + select.push(entity({ + id: guid('menu', 100), + path: '/ui/DefaultGroup/CharacterSelectHud', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 21, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, type: 1, raycast: true }), + ], + })); + select.push(entity({ + id: guid('menu', 190), + path: '/ui/DefaultGroup/CharacterSelectHud/OpaqueBackdrop', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + ], + })); + select.push(entity({ + id: guid('menu', 101), + path: '/ui/DefaultGroup/CharacterSelectHud/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 72 }, pos: { x: 0, y: 355 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT }), + text({ value: '\uCE90\uB9AD\uD130 \uC120\uD0DD', fontSize: 42, bold: true, color: GOLD, alignment: 0 }), + ], + })); + select.push(entity({ + id: guid('menu', 102), + path: '/ui/DefaultGroup/CharacterSelectHud/Status', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 680, y: 44 }, pos: { x: 0, y: 298 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT }), + text({ value: '\uC804\uC0AC\uB97C \uC120\uD0DD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694', fontSize: 22, color: { r: 0.86, g: 0.9, b: 0.94, a: 1 }, alignment: 0 }), + ], + })); + const classCards = [ + { key: 'Warrior', classId: 'warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } }, + { key: 'Thief', classId: 'bandit', label: '\uB3C4\uC801', desc: '\uB3C5\u00B7\uB2E8\uAC80\u00B7\uB4DC\uB85C\uC6B0', x: 0, enabled: true, tint: { r: 0.26, g: 0.5, b: 0.34, a: 1 } }, + { key: 'Mage', classId: 'magician', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } }, + ]; + for (let i = 0; i < classCards.length; i++) { + const cls = classCards[i]; + const base = `/ui/DefaultGroup/CharacterSelectHud/${cls.key}Button`; + select.push(entity({ + id: guid('menu', 110 + i), + path: base, + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 10 + i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 270, y: 330 }, pos: { x: cls.x, y: 40 }, align: ALIGN_CENTER }), + sprite({ color: cls.enabled ? { r: 0.16, g: 0.2, b: 0.26, a: 1 } : { r: 0.11, g: 0.12, b: 0.14, a: 1 }, type: 1, raycast: cls.enabled }), + button({ enabled: cls.enabled }), + ], + })); + select.push(entity({ + id: guid('menu', 200 + i), + path: `${base}/Art`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 318 }, pos: { x: 0, y: 0 } }), + sprite({ dataId: CHARS.portraits[cls.classId], color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: false }), + ], + })); + select.push(entity({ + id: guid('menu', 210 + i), + path: `${base}/NameBanner`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 1, + components: [ + transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 258, y: 60 }, pos: { x: 0, y: -137 } }), + sprite({ color: { r: 0, g: 0, b: 0, a: 0.55 }, type: 1, raycast: false }), + ], + })); + select.push(entity({ + id: guid('menu', 120 + i), + path: `${base}/Name`, + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 230, y: 54 }, pos: { x: 0, y: -137 } }), + sprite({ color: TRANSPARENT }), + text({ value: cls.label, fontSize: 34, bold: true, color: cls.enabled ? GOLD : { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 4 }), + ], + })); + if (!cls.enabled) { + select.push(entity({ + id: guid('menu', 150 + i), + path: `${base}/LockBody`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 3, + components: [ + transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 76, y: 58 }, pos: { x: 0, y: 4 } }), + sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }), + ], + })); + select.push(entity({ + id: guid('menu', 160 + i), + path: `${base}/LockShackle`, + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 4, + components: [ + transform({ parentW: 270, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 54, y: 42 }, pos: { x: 0, y: 48 } }), + sprite({ color: { r: 0.78, g: 0.69, b: 0.42, a: 1 }, type: 1 }), + ], + })); + } + select.push(entity({ + id: guid('menu', 170 + i), + path: `/ui/DefaultGroup/CharacterSelectHud/${cls.key}DeckButton`, + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 18 + i, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 160, y: 46 }, pos: { x: cls.x, y: -160 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.11, g: 0.13, b: 0.16, a: 1 }, type: 1, raycast: true }), + button({ enabled: cls.enabled }), + text({ value: '\uB371 \uBCF4\uAE30', fontSize: 20, bold: true, color: GOLD, alignment: 0 }), + ], + })); + } + select.push(entity({ + id: guid('menu', 180), + path: '/ui/DefaultGroup/CharacterSelectHud/StartButton', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 20, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 68 }, pos: { x: 720, y: -360 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '\uC2DC\uC791', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), + ], + })); + select.push(entity({ + id: guid('menu', 230), + path: '/ui/DefaultGroup/CharacterSelectHud/BackButton', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 22, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 56 }, pos: { x: -800, y: 430 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.15, g: 0.2, b: 0.26, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '\u2190 \uB4A4\uB85C', fontSize: 26, bold: true, color: GOLD, alignment: 0 }), + ], + })); + select[0].jsonString.enable = false; + return select; +} diff --git a/tools/deck/hud/mainmenu.mjs b/tools/deck/hud/mainmenu.mjs new file mode 100644 index 0000000..df87f0d --- /dev/null +++ b/tools/deck/hud/mainmenu.mjs @@ -0,0 +1,127 @@ +import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; +import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; + +export function buildMainMenu() { + const menu = []; + menu.push(entity({ + id: guid('menu', 0), + path: '/ui/DefaultGroup/MainMenu', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 20, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.06, g: 0.09, b: 0.13, a: 1 }, type: 1, raycast: true }), + ], + })); + menu.push(entity({ + id: guid('menu', 50), + path: '/ui/DefaultGroup/MainMenu/OpaqueBackdrop', + modelId: 'uisprite', + entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT, type: 1, raycast: false }), + ], + })); + menu.push(entity({ + id: guid('menu', 1), + path: '/ui/DefaultGroup/MainMenu/Title', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 720, y: 100 }, pos: { x: 0, y: 180 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT }), + text({ value: '슬레이 메이플', fontSize: 64, bold: true, color: GOLD, alignment: 0 }), + ], + })); + menu.push(entity({ + id: guid('menu', 2), + path: '/ui/DefaultGroup/MainMenu/Subtitle', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 48 }, pos: { x: 0, y: 104 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT }), + text({ value: '카드를 뽑고, 덱을 만들고, 첨탑을 오른다', fontSize: 24, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 0 }), + ], + })); + menu.push(entity({ + id: guid('menu', 3), + path: '/ui/DefaultGroup/MainMenu/NewGameButton', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 68 }, pos: { x: 0, y: -20 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '새 게임', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), + ], + })); + // 승천 선택 (P11): [-] 라벨 [+] + menu.push(entity({ + id: guid('menu', 195), + path: '/ui/DefaultGroup/MainMenu/AscMinus', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 5, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: -170, y: -185 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '-', fontSize: 30, bold: true, color: GOLD, alignment: 4 }), + ], + })); + menu.push(entity({ + id: guid('menu', 196), + path: '/ui/DefaultGroup/MainMenu/AscLabel', + modelId: 'uitext', + entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 6, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 250, y: 40 }, pos: { x: 0, y: -185 }, align: ALIGN_CENTER }), + sprite({ color: TRANSPARENT }), + text({ value: '승천 0 / 해금 0', fontSize: 22, bold: true, color: { r: 0.85, g: 0.7, b: 0.95, a: 1 }, alignment: 4 }), + ], + })); + menu.push(entity({ + id: guid('menu', 197), + path: '/ui/DefaultGroup/MainMenu/AscPlus', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 7, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 52, y: 52 }, pos: { x: 170, y: -185 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), + button(), + text({ value: '+', fontSize: 30, bold: true, color: GOLD, alignment: 4 }), + ], + })); + menu.push(entity({ + id: guid('menu', 4), + path: '/ui/DefaultGroup/MainMenu/ContinueButton', + modelId: 'uibutton', + entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 3, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 58 }, pos: { x: 0, y: -100 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.1, g: 0.11, b: 0.13, a: 0.78 }, type: 1, raycast: false }), + button({ enabled: false }), + text({ value: '이어하기', fontSize: 24, bold: true, color: { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 0 }), + ], + })); + return menu; +} -- 2.49.1 From d82e98f8326ab263db0220bacc3563a3d2aaaddc Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 02:42:20 +0900 Subject: [PATCH 7/8] =?UTF-8?q?docs(harness):=20RULES=20=C2=A71=EC=97=90?= =?UTF-8?q?=20gen-slaydeck=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0(lib/?= =?UTF-8?q?=C2=B7hud/)=C2=B7diffcheck=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RULES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RULES.md b/RULES.md index f789ffd..4df7c43 100644 --- a/RULES.md +++ b/RULES.md @@ -11,7 +11,7 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된 | 산출물 (절대 Read/Edit 금지) | 크기 | 단일 소스 (여기만 편집) | 재생성 명령 | |---|---|---|---| -| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/gen-slaydeck.mjs` | `node tools/deck/gen-slaydeck.mjs` | +| `ui/DefaultGroup.ui` | **~7.1MB** | `data/*.json` + `tools/deck/`(`gen-slaydeck.mjs`+`lib/`+`hud/`) | `node tools/deck/gen-slaydeck.mjs` | | `RootDesk/MyDesk/SlayDeckController.codeblock` | ~270KB | 〃 | 〃 | | `Global/common.gamelogic` | ~1KB | 〃 | 〃 | | `map/map01.map`~`map05.map`, `map/lobby.map` | 각 ~210KB | `tools/map/`·`tools/monster/`·`tools/camera/`·`tools/player/` (↓ 보조 생성기) | 해당 생성기 | @@ -22,7 +22,9 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된 | `Global/SectorConfig.config` | ~1KB | `tools/map/gen-maps.mjs`·`gen-lobby-map.mjs` (패치) | 해당 생성기 | - `.claude/settings.json`의 permissions.deny가 위 파일의 Read/Edit/Write 도구 사용을 차단한다 (이 저장소를 열면 자동 적용). deny는 **glob** — `ui/*.ui`·`map/*.map`·`RootDesk/MyDesk/*.codeblock`·`Global/common.gamelogic`·`Global/SectorConfig.config`. 따라서 **메이커 저작 codeblock/UI**(`Monster`·`MonsterAttack`·`PlayerAttack`·`PlayerHit`·`UIPopup`·`UIToast`.codeblock, `ui/PopupGroup.ui`·`ui/ToastGroup.ui`)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발. -- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(생성기 JS) 또는 `data/*.json`(데이터)을 수정** → 재생성 → 산출물은 통째로 커밋. +- 게임 로직·UI 수정 = **`tools/deck/gen-slaydeck.mjs`(오케스트레이터 + codeblock Lua) 또는 `data/*.json`(데이터)을 수정** → 재생성 → 산출물은 통째로 커밋. + - **UI emit은 HUD별 모듈** `tools/deck/hud/*.mjs`(charselect·shop·combat·map·deckall·soulshop 등 16종), **공유 헬퍼·상수·데이터·lua 테이블은** `tools/deck/lib/ui-helpers.mjs`·`tools/deck/lib/data.mjs`. 특정 화면 UI 수정은 해당 `hud/.mjs`만 손대면 된다(의존: orchestrator→hud→lib 단방향). + - 리팩터 시 **출력 바이트-동일 검증**: `node tools/deck/gen-slaydeck.mjs` 후 `node tools/verify/diffcheck.mjs`(워킹트리 vs HEAD 줄바꿈 정규화 비교 — 산출물 경로를 명령줄에 노출 안 해 deny 회피). 산출물 ` M`은 보통 autocrlf churn이니 `git checkout --`로 복원. - **보조 생성기**(각자 자기 산출물의 단일 소스 — 위 표의 메인 `gen-slaydeck.mjs` 외): - `tools/camera/gen-camera.mjs` → `MapCamera.codeblock` + map01~05 카메라 부착 (값 `data/camera.json`) - `tools/map/gen-maps.mjs` → `map02~05` + `Global/SectorConfig.config` (map01 템플릿 클론) -- 2.49.1 From d265c8f918814679e6b4c03dd01f0f97525de9d6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 07:32:20 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore(verify):=20diffcheck=EC=97=90=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=20ref=20=EC=9D=B8=EC=9E=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(=EA=B8=B0=EB=B3=B8=20HEAD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/verify/diffcheck.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/verify/diffcheck.mjs b/tools/verify/diffcheck.mjs index 25889c7..3f2464e 100644 --- a/tools/verify/diffcheck.mjs +++ b/tools/verify/diffcheck.mjs @@ -1,9 +1,10 @@ import { readFileSync } from 'node:fs'; import { execSync } from 'node:child_process'; -// 산출물 바이트-동일 게이트: 워킹트리 vs HEAD(blob)를 줄바꿈 정규화 후 비교. +// 산출물 바이트-동일 게이트: 워킹트리 vs 지정 ref(blob)를 줄바꿈 정규화 후 비교. // 산출물 경로를 Bash 명령줄에 노출하지 않아 settings.json deny를 회피(count.mjs와 동일 취지). -// 사용: node tools/verify/diffcheck.mjs +// 사용: node tools/verify/diffcheck.mjs [ref] (ref 기본 HEAD; 예: origin/main) +const ref = process.argv[2] || 'HEAD'; const FILES = [ 'ui/DefaultGroup.ui', 'RootDesk/MyDesk/SlayDeckController.codeblock', @@ -12,9 +13,9 @@ const FILES = [ let allSame = true; for (const f of FILES) { const work = readFileSync(f, 'utf8').replace(/\r\n/g, '\n'); - const blob = execSync(`git show HEAD:${f}`, { encoding: 'utf8', maxBuffer: 1 << 30 }).replace(/\r\n/g, '\n'); + const blob = execSync(`git show ${ref}:${f}`, { encoding: 'utf8', maxBuffer: 1 << 30 }).replace(/\r\n/g, '\n'); const same = work === blob; if (!same) allSame = false; - console.log(`${same ? 'IDENTICAL ' : 'DIFFER '} ${f}${same ? '' : ` (work=${work.length} blob=${blob.length})`}`); + console.log(`${same ? 'IDENTICAL ' : 'DIFFER '} ${f}${same ? '' : ` (work=${work.length} ${ref}=${blob.length})`}`); } -console.log(allSame ? '\n=> 산출물 바이트-동일 (리팩터 안전)' : '\n=> 차이 있음 (내용 변경 — 확인 필요)'); +console.log(allSame ? `\n=> 산출물이 ${ref}와 바이트-동일` : `\n=> ${ref}와 차이 있음 (확인 필요)`); -- 2.49.1