refactor: SlayDeckController 관심사별 모듈 분리 + 코드 규칙 3종 #94

Merged
gahusb merged 3 commits from refactor/cb-concern-modules into main 2026-06-29 17:39:19 +09:00
10 changed files with 543 additions and 472 deletions

View File

@@ -23,7 +23,7 @@ Claude Code는 `CLAUDE.md`가 이 파일을 임포트하므로 자동 적용된
- `.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/*.ui`** — UI는 6개 UIGroup으로 메이커 저작)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발. - `.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/*.ui`** — UI는 6개 UIGroup으로 메이커 저작)**도** Read/Edit 금지 — 이들은 생성기가 없으니 **메이커에서** 편집한다(텍스트 도구로 X). codeblock은 한 줄짜리 JSON이라 Read 시 토큰 폭발.
- **게임 로직 수정** = `tools/deck/gen-slaydeck.mjs`(오케스트레이터) + `tools/deck/cb/*.mjs`(codeblock Lua) 또는 `data/*.json`(데이터) 수정 → 재생성(`SlayDeckController.codeblock`+`common.gamelogic`만, **`.ui` 미접근**) → 통째로 커밋. **UI 수정 = 메이커에서**(생성기는 UI를 안 만든다). - **게임 로직 수정** = `tools/deck/gen-slaydeck.mjs`(오케스트레이터) + `tools/deck/cb/*.mjs`(codeblock Lua) 또는 `data/*.json`(데이터) 수정 → 재생성(`SlayDeckController.codeblock`+`common.gamelogic`만, **`.ui` 미접근**) → 통째로 커밋. **UI 수정 = 메이커에서**(생성기는 UI를 안 만든다).
- **codeblock 메서드(Lua)는 기능별 모듈** `tools/deck/cb/*.mjs`(boot·state·combat·hand·deckview·items·map·shop 등 17종). **공유분**: 상수·데이터·lua 테이블 = `tools/deck/lib/{ui-helpers,data,codeblock}.mjs`(cb가 import — `MAX_MONSTERS`=4 등). prop 103개는 오케스트레이터 `writeCodeblocks`에 유지. 특정 메서드 수정은 `cb/<name>.mjs`만(의존: orchestrator→cb→lib 단방향). **cb 모듈은 원본 메서드 순서 보존이 바이트동일 조건**. **UI emit(옛 `hud/*.mjs` 15종·`gen-cardhand.mjs`)은 `tools/deck/legacy/`로 이관 — 휴면(생성기 미사용)**: UI가 메이커 저작이라 생성기가 안 만든다. (롤백용 `legacy/upsert-ui.mjs`는 직접 실행 시에만 옛 `DefaultGroup.ui`를 재생성.) - **codeblock 메서드(Lua)는 관심사별 모듈** `tools/deck/cb/*.mjs`(boot·screens·npc·navigation·layout·combat·hand·deckview·items·map·shop 등 20종 — 화면전환=`screens`·NPC=`npc`·포지션=`navigation`(월드 텔레포트)/`layout`(UI 슬롯 배치). 새 메서드는 관심사에 맞는 모듈에 작성하고, 한 모듈이 비대해지면 분할한다. 횡단 관심사를 한 모듈에 몰아넣지 않는다). **공유분**: 상수·데이터·lua 테이블 = `tools/deck/lib/{ui-helpers,data,codeblock}.mjs`(cb가 import — `MAX_MONSTERS`=4 등). prop 103개는 오케스트레이터 `writeCodeblocks`에 유지. 특정 메서드 수정은 `cb/<name>.mjs`만(의존: orchestrator→cb→lib 단방향). **cb 모듈은 원본 메서드 순서 보존이 바이트동일 조건**. **UI emit(옛 `hud/*.mjs` 15종·`gen-cardhand.mjs`)은 `tools/deck/legacy/`로 이관 — 휴면(생성기 미사용)**: UI가 메이커 저작이라 생성기가 안 만든다. (롤백용 `legacy/upsert-ui.mjs`는 직접 실행 시에만 옛 `DefaultGroup.ui`를 재생성.)
- 리팩터 시 **출력 바이트-동일 검증**: `node tools/deck/gen-slaydeck.mjs``node tools/verify/diffcheck.mjs [ref]`(워킹트리 vs ref(기본 HEAD) 줄바꿈 정규화 비교 — 산출물 경로를 명령줄에 노출 안 해 deny 회피). 산출물 ` M`은 보통 autocrlf churn이니 `git checkout --`로 복원. - 리팩터 시 **출력 바이트-동일 검증**: `node tools/deck/gen-slaydeck.mjs``node tools/verify/diffcheck.mjs [ref]`(워킹트리 vs ref(기본 HEAD) 줄바꿈 정규화 비교 — 산출물 경로를 명령줄에 노출 안 해 deny 회피). 산출물 ` M`은 보통 autocrlf churn이니 `git checkout --`로 복원.
- **UI 전면 메이커 저작 (2026-06-17~)**: 단일 `DefaultGroup`을 7개 UIGroup으로 분리 — `DefaultGroup`(MainMenu+월드조작), `SelectUIGroup`(charselect/job), `LobbyUIGroup`(lobby/board/soulshop), `RunUIGroup`(combat/map/shop/rest/treasure/reward/cardhand/deck), `DeckUIGroup`(덱 도감), `PopupGroup`·`ToastGroup`. 컨트롤러(`cb/*.mjs`)는 엔티티 **경로**(`/ui/<UIGroup>/<Hud>/...`)로 텍스트·이미지·표시숨김·상태기반 위치/크기/색을 **런타임 주입**(레이아웃=메이커, 내용=컨트롤러 — 메이커가 이 경로 유지 필수). 몬스터 슬롯 = `RunUIGroup/CombatHud/MonsterStatus{1..4}`(자식 Name·Hp·Intent·HpBarFill·Buffs·BlockBadge·TargetMarker; TargetFrame 없음). **부트 흐름**: `OnBeginPlay`→MainMenu→(`MainMenu/NewGameButton`)→로비→run NPC(`OnLobbyNpcInteract` id=="run")→charselect→런. **재연결 검증**: `node tools/verify/cbgap.mjs`(cb 참조 경로↔.ui GAP 0이어야) + 재생성 후 `git status -- ui/` 변경 0(생성기 .ui 미접근 증명). 섹션→UIGroup 일괄 remap 마이그레이션은 `tools/deck/reconnect-ui-paths.mjs`(멱등). UIGroup별 .ui 분포 확인은 `tools/verify/uimap.mjs`. - **UI 전면 메이커 저작 (2026-06-17~)**: 단일 `DefaultGroup`을 7개 UIGroup으로 분리 — `DefaultGroup`(MainMenu+월드조작), `SelectUIGroup`(charselect/job), `LobbyUIGroup`(lobby/board/soulshop), `RunUIGroup`(combat/map/shop/rest/treasure/reward/cardhand/deck), `DeckUIGroup`(덱 도감), `PopupGroup`·`ToastGroup`. 컨트롤러(`cb/*.mjs`)는 엔티티 **경로**(`/ui/<UIGroup>/<Hud>/...`)로 텍스트·이미지·표시숨김·상태기반 위치/크기/색을 **런타임 주입**(레이아웃=메이커, 내용=컨트롤러 — 메이커가 이 경로 유지 필수). 몬스터 슬롯 = `RunUIGroup/CombatHud/MonsterStatus{1..4}`(자식 Name·Hp·Intent·HpBarFill·Buffs·BlockBadge·TargetMarker; TargetFrame 없음). **부트 흐름**: `OnBeginPlay`→MainMenu→(`MainMenu/NewGameButton`)→로비→run NPC(`OnLobbyNpcInteract` id=="run")→charselect→런. **재연결 검증**: `node tools/verify/cbgap.mjs`(cb 참조 경로↔.ui GAP 0이어야) + 재생성 후 `git status -- ui/` 변경 0(생성기 .ui 미접근 증명). 섹션→UIGroup 일괄 remap 마이그레이션은 `tools/deck/reconnect-ui-paths.mjs`(멱등). UIGroup별 .ui 분포 확인은 `tools/verify/uimap.mjs`.
- **머지 충돌(gen-slaydeck.mjs)**: 다른 브랜치가 단일체를 수정해 충돌나면, 그쪽 버전(`git checkout --theirs tools/deck/gen-slaydeck.mjs`)을 취해 **콘텐츠 마커 기반으로 재모듈화**(라인인덱스 X — 줄 추가에 안전·export 이름 자동 파생·`const x=[]` 직전 전문 상수 walk-back 포함) 후 `node tools/verify/diffcheck.mjs origin/main`으로 ui·codeblock 바이트-동일 확인(손실 0 증명). codeblock 메서드·patchCommon은 오케스트레이터 잔류라 그쪽 변경은 자동 보존됨. - **머지 충돌(gen-slaydeck.mjs)**: 다른 브랜치가 단일체를 수정해 충돌나면, 그쪽 버전(`git checkout --theirs tools/deck/gen-slaydeck.mjs`)을 취해 **콘텐츠 마커 기반으로 재모듈화**(라인인덱스 X — 줄 추가에 안전·export 이름 자동 파생·`const x=[]` 직전 전문 상수 walk-back 포함) 후 `node tools/verify/diffcheck.mjs origin/main`으로 ui·codeblock 바이트-동일 확인(손실 0 증명). codeblock 메서드·patchCommon은 오케스트레이터 잔류라 그쪽 변경은 자동 보존됨.
@@ -65,6 +65,7 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
- 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`. - 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create <spec.json>` / `merge <번호>`.
- PR 제목과 본문은 한국어로 작성한다. - PR 제목과 본문은 한국어로 작성한다.
- 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시. - 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시.
- **PR 머지 후 브랜치 삭제**: 머지된 `feature/*`·`docs/*` 브랜치는 로컬·원격 모두 삭제한다. 삭제 전 `git merge-base --is-ancestor origin/<브랜치> origin/main`로 완전 머지 확인(종료코드 0=완전 머지 → 삭제 가능). main에 없는 커밋이 남은 브랜치와 `codex/*` 등 작업 중 브랜치는 보존한다.
## 5. 메이커(MSW) 연동 주의 ## 5. 메이커(MSW) 연동 주의
@@ -86,3 +87,9 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock
- UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다. - UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다.
- 생성기 내 Lua UI 코드에서 number 또는 숫자 문자열을 텍스트에 붙일 때는 `FormatNumber` 같은 포맷 헬퍼를 우선 사용한다. - 생성기 내 Lua UI 코드에서 number 또는 숫자 문자열을 텍스트에 붙일 때는 `FormatNumber` 같은 포맷 헬퍼를 우선 사용한다.
- 소수부가 플레이어에게 의미 있을 때만 소수점 표기를 유지한다. - 소수부가 플레이어에게 의미 있을 때만 소수점 표기를 유지한다.
## 8. codeblock 변수명
- cb(`tools/deck/cb/*.mjs`)의 Lua 지역변수는 **의미가 드러나는 이름**으로 작성한다(`e``entity`, `n``count`, `m``monster`, `lp``localPlayer`, `s``soulPoints`, `tr``transform`). `a`/`b`/`c` 같은 무의미 단일문자 변수는 금지.
- 단, 순수 반복 인덱스 `i`/`j`/`r`/`c`는 관용상 허용한다.
- 새 cb 메서드를 작성하거나 기존 메서드를 손댈 때 이 규칙을 적용한다(대규모 일괄 개명은 별도 작업으로).

View File

@@ -1268,7 +1268,7 @@
"Name": null "Name": null
}, },
"Arguments": [], "Arguments": [],
"Code": "local function grp(n)\n\tlocal g = _EntityService:GetEntityByPath(\"/ui/\" .. n)\n\tif g ~= nil then g:SetEnable(true) end\nend\ngrp(\"SelectUIGroup\")\ngrp(\"LobbyUIGroup\")\ngrp(\"RunUIGroup\")\ngrp(\"DeckUIGroup\")", "Code": "local function enableGroup(name)\n\tlocal group = _EntityService:GetEntityByPath(\"/ui/\" .. name)\n\tif group ~= nil then group:SetEnable(true) end\nend\nenableGroup(\"SelectUIGroup\")\nenableGroup(\"LobbyUIGroup\")\nenableGroup(\"RunUIGroup\")\nenableGroup(\"DeckUIGroup\")",
"Scope": 2, "Scope": 2,
"ExecSpace": 2, "ExecSpace": 2,
"Attributes": [], "Attributes": [],
@@ -1351,45 +1351,7 @@
"Name": null "Name": null
}, },
"Arguments": [], "Arguments": [],
"Code": "self.LobbyTpTries = 0\nlocal eventId = 0\nlocal function go()\n\tself.LobbyTpTries = self.LobbyTpTries + 1\n\tlocal lp = _UserService.LocalPlayer\n\tif lp ~= nil then\n\t\tif lp.CurrentMapName ~= \"lobby\" then\n\t\t\t_TeleportService:TeleportToMapPosition(lp, Vector3(-5, 0.03, 0), \"lobby\")\n\t\tend\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.LobbyTpTries > 50 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(go, 0.1)", "Code": "local soulPoints = self.SoulPoints or 0\nself:SetText(\"/ui/LobbyUIGroup/LobbyHud/SoulLabel\", \"영혼 \" .. string.format(\"%d\", soulPoints))\nself:SetText(\"/ui/LobbyUIGroup/SoulShopHud/Souls\", \"영혼 \" .. string.format(\"%d\", soulPoints))",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "GoLobbyMap"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "string",
"DefaultValue": "\"\"",
"SyncDirection": 0,
"Attributes": [],
"Name": "id"
}
],
"Code": "if self.RunActive == true then\n\treturn\nend\nif id == \"run\" then\n\tself:ShowCharacterSelect()\nelseif id == \"codex\" then\n\tself:ShowCodex()\nelseif id == \"shop\" then\n\tself:ShowSoulShop()\nelseif id == \"board\" then\n\tself:ShowBoard()\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "OnLobbyNpcInteract"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "local s = self.SoulPoints or 0\nself:SetText(\"/ui/LobbyUIGroup/LobbyHud/SoulLabel\", \"영혼 \" .. string.format(\"%d\", s))\nself:SetText(\"/ui/LobbyUIGroup/SoulShopHud/Souls\", \"영혼 \" .. string.format(\"%d\", s))",
"Scope": 2, "Scope": 2,
"ExecSpace": 6, "ExecSpace": 6,
"Attributes": [], "Attributes": [],
@@ -1404,7 +1366,7 @@
"Name": null "Name": null
}, },
"Arguments": [], "Arguments": [],
"Code": "if self.LobbyBound == true then\n\treturn\nend\nself.LobbyBound = true\nlocal function bindClick(path, fn)\n\tlocal e = _EntityService:GetEntityByPath(path)\n\tif e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent(\"ButtonComponent\") ~= nil) then\n\t\te:ConnectEvent(ButtonClickEvent, fn)\n\tend\nend\nbindClick(\"/ui/LobbyUIGroup/LobbyHud/AscMinus\", function() self:AdjustAscension(-1) end)\nbindClick(\"/ui/LobbyUIGroup/LobbyHud/AscPlus\", function() self:AdjustAscension(1) end)\nbindClick(\"/ui/LobbyUIGroup/BoardHud/Close\", function() self:CloseBoard() end)\nbindClick(\"/ui/LobbyUIGroup/SoulShopHud/Close\", function() self:CloseSoulShop() end)", "Code": "if self.LobbyBound == true then\n\treturn\nend\nself.LobbyBound = true\nlocal function bindClick(path, handler)\n\tlocal entity = _EntityService:GetEntityByPath(path)\n\tif entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent(\"ButtonComponent\") ~= nil) then\n\t\tentity:ConnectEvent(ButtonClickEvent, handler)\n\tend\nend\nbindClick(\"/ui/LobbyUIGroup/LobbyHud/AscMinus\", function() self:AdjustAscension(-1) end)\nbindClick(\"/ui/LobbyUIGroup/LobbyHud/AscPlus\", function() self:AdjustAscension(1) end)\nbindClick(\"/ui/LobbyUIGroup/BoardHud/Close\", function() self:CloseBoard() end)\nbindClick(\"/ui/LobbyUIGroup/SoulShopHud/Close\", function() self:CloseSoulShop() end)",
"Scope": 2, "Scope": 2,
"ExecSpace": 6, "ExecSpace": 6,
"Attributes": [], "Attributes": [],
@@ -1455,6 +1417,59 @@
"Attributes": [], "Attributes": [],
"Name": "CloseBoard" "Name": "CloseBoard"
}, },
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "string",
"DefaultValue": "\"\"",
"SyncDirection": 0,
"Attributes": [],
"Name": "id"
}
],
"Code": "if self.RunActive == true then\n\treturn\nend\nif id == \"run\" then\n\tself:ShowCharacterSelect()\nelseif id == \"codex\" then\n\tself:ShowCodex()\nelseif id == \"shop\" then\n\tself:ShowSoulShop()\nelseif id == \"board\" then\n\tself:ShowBoard()\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "OnLobbyNpcInteract"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self.LobbyTpTries = 0\nlocal eventId = 0\nlocal function tryTeleport()\n\tself.LobbyTpTries = self.LobbyTpTries + 1\n\tlocal localPlayer = _UserService.LocalPlayer\n\tif localPlayer ~= nil then\n\t\tif localPlayer.CurrentMapName ~= \"lobby\" then\n\t\t\t_TeleportService:TeleportToMapPosition(localPlayer, Vector3(-5, 0.03, 0), \"lobby\")\n\t\tend\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.LobbyTpTries > 50 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(tryTeleport, 0.1)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "GoLobbyMap"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "local maps = { \"map01\", \"map02\", \"map03\", \"map04\", \"map05\" }\nlocal target = maps[self.Floor]\nif target == nil then\n\treturn\nend\nlocal localPlayer = _UserService.LocalPlayer\nif localPlayer == nil then\n\treturn\nend\nif localPlayer.CurrentMapName == target then\n\treturn\nend\n_TeleportService:TeleportToMapPosition(localPlayer, Vector3(-6, 0.03, 0), target)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "TeleportToActMap"
},
{ {
"Return": { "Return": {
"Type": "void", "Type": "void",
@@ -3937,21 +3952,6 @@
"Attributes": [], "Attributes": [],
"Name": "SetJob" "Name": "SetJob"
}, },
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "local maps = { \"map01\", \"map02\", \"map03\", \"map04\", \"map05\" }\nlocal target = maps[self.Floor]\nif target == nil then\n\treturn\nend\nlocal lp = _UserService.LocalPlayer\nif lp == nil then\n\treturn\nend\nif lp.CurrentMapName == target then\n\treturn\nend\n_TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "TeleportToActMap"
},
{ {
"Return": { "Return": {
"Type": "void", "Type": "void",
@@ -4230,29 +4230,6 @@
"Attributes": [], "Attributes": [],
"Name": "SetHpBar" "Name": "SetHpBar"
}, },
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "number",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "slot"
}
],
"Code": "local m = self.Monsters[slot]\nif m == nil or m.entity == nil or not isvalid(m.entity) then\n\treturn\nend\nlocal tr = m.entity.TransformComponent\nif tr == nil then\n\treturn\nend\nlocal wp = tr.WorldPosition\nlocal screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 1.4))\nlocal uipos = _UILogic:ScreenToUIPosition(screen)\nlocal e = _EntityService:GetEntityByPath(\"/ui/RunUIGroup/CombatHud/MonsterStatus\" .. tostring(slot))\nif e ~= nil and e.UITransformComponent ~= nil then\n\te.UITransformComponent.anchoredPosition = uipos\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "PositionMonsterSlot"
},
{ {
"Return": { "Return": {
"Type": "void", "Type": "void",
@@ -4291,6 +4268,29 @@
"Attributes": [], "Attributes": [],
"Name": "RenderRun" "Name": "RenderRun"
}, },
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "number",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "slot"
}
],
"Code": "local monster = self.Monsters[slot]\nif monster == nil or monster.entity == nil or not isvalid(monster.entity) then\n\treturn\nend\nlocal transform = monster.entity.TransformComponent\nif transform == nil then\n\treturn\nend\nlocal worldPos = transform.WorldPosition\nlocal screen = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + 1.4))\nlocal uipos = _UILogic:ScreenToUIPosition(screen)\nlocal slotEntity = _EntityService:GetEntityByPath(\"/ui/RunUIGroup/CombatHud/MonsterStatus\" .. tostring(slot))\nif slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then\n\tslotEntity.UITransformComponent.anchoredPosition = uipos\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "PositionMonsterSlot"
},
{ {
"Return": { "Return": {
"Type": "any", "Type": "any",

21
tools/deck/cb/layout.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.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';
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';
export const layoutMethods = [
method('PositionMonsterSlot', `local monster = self.Monsters[slot]
if monster == nil or monster.entity == nil or not isvalid(monster.entity) then
return
end
local transform = monster.entity.TransformComponent
if transform == nil then
return
end
local worldPos = transform.WorldPosition
local screen = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + ${HEAD_OFFSET_Y}))
local uipos = _UILogic:ScreenToUIPosition(screen)
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot))
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
slotEntity.UITransformComponent.anchoredPosition = uipos
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
];

View File

@@ -0,0 +1,34 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.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';
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';
export const navigationMethods = [
method('GoLobbyMap', `self.LobbyTpTries = 0
local eventId = 0
local function tryTeleport()
self.LobbyTpTries = self.LobbyTpTries + 1
local localPlayer = _UserService.LocalPlayer
if localPlayer ~= nil then
if localPlayer.CurrentMapName ~= "${LOBBY_MAP}" then
_TeleportService:TeleportToMapPosition(localPlayer, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
end
_TimerService:ClearTimer(eventId)
elseif self.LobbyTpTries > 50 then
_TimerService:ClearTimer(eventId)
end
end
eventId = _TimerService:SetTimerRepeat(tryTeleport, 0.1)`),
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((mapName) => `"${mapName}"`).join(', ')} }
local target = maps[self.Floor]
if target == nil then
return
end
local localPlayer = _UserService.LocalPlayer
if localPlayer == nil then
return
end
if localPlayer.CurrentMapName == target then
return
end
_TeleportService:TeleportToMapPosition(localPlayer, Vector3(-6, 0.03, 0), target)`),
];

18
tools/deck/cb/npc.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.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';
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';
export const npcMethods = [
method('OnLobbyNpcInteract', `if self.RunActive == true then
return
end
if id == "run" then
self:ShowCharacterSelect()
elseif id == "codex" then
self:ShowCodex()
elseif id == "shop" then
self:ShowSoulShop()
elseif id == "board" then
self:ShowBoard()
end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]),
];

View File

@@ -1,311 +1,296 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs'; import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.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'; 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 { 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 { 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';
export const renderMethods = [ export const renderMethods = [
method('BuffsLabel', `local parts = {} method('BuffsLabel', `local parts = {}
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
return table.concat(parts, " ")`, [ return table.concat(parts, " ")`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
], 0, 'string'), ], 0, 'string'),
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i)
local m = self.Monsters[i] local m = self.Monsters[i]
if m ~= nil and m.alive == true then if m ~= nil and m.alive == true then
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
self:SetText(base .. "/Name", m.name) self:SetText(base .. "/Name", m.name)
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp)) self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
local intent = m.intents[m.intentIdx] local intent = m.intents[m.intentIdx]
local t = "" local t = ""
if intent ~= nil then if intent ~= nil then
if intent.kind == "Attack" then if intent.kind == "Attack" then
local atk = intent.value + m.str local atk = intent.value + m.str
if m.weak > 0 then atk = math.floor(atk * 0.75) end if m.weak > 0 then atk = math.floor(atk * 0.75) end
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
t = "공격 " .. tostring(atk) t = "공격 " .. tostring(atk)
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value) elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
elseif intent.kind == "Debuff" then elseif intent.kind == "Debuff" then
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여" if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
else t = "취약 " .. tostring(intent.value) .. " 부여" end else t = "취약 " .. tostring(intent.value) .. " 부여" end
elseif intent.kind == "AddCard" then elseif intent.kind == "AddCard" then
t = "저주 카드 추가" t = "저주 카드 추가"
end end
end end
self:SetText(base .. "/Intent", t) self:SetText(base .. "/Intent", t)
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
local shownTarget = self.TargetIndex local shownTarget = self.TargetIndex
if dragActive == true then shownTarget = self.DragTargetIndex end if dragActive == true then shownTarget = self.DragTargetIndex end
self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive) self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive)
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive) self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent") local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
if intent.kind == "Attack" then if intent.kind == "Attack" then
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1) intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
elseif intent.kind == "Debuff" then elseif intent.kind == "Debuff" then
intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1) intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1)
elseif intent.kind == "AddCard" then elseif intent.kind == "AddCard" then
intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1) intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1)
else else
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1) intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
end end
end end
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W}) self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0) self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block)) self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0)) self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
else else
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, false)
end end
end end
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp)) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
self:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220) self:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "불가침" .. tostring(self.PlayerIntangible) pb = pb .. "불가침" .. tostring(self.PlayerIntangible)
end end
if self.PlayerDex ~= nil and self.PlayerDex > 0 then if self.PlayerDex ~= nil and self.PlayerDex > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "민첩+" .. tostring(self.PlayerDex) pb = pb .. "민첩+" .. tostring(self.PlayerDex)
end end
if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "가시" .. tostring(self.PlayerThorns) pb = pb .. "가시" .. tostring(self.PlayerThorns)
end end
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
local names = {} local names = {}
for i = 1, #self.PlayerPowers do for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]] local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil then table.insert(names, pc.name) end if pc ~= nil then table.insert(names, pc.name) end
end end
if pb ~= "" then pb = pb .. " · " end if pb ~= "" then pb = pb .. " · " end
pb = pb .. table.concat(names, " ") pb = pb .. table.concat(names, " ")
end end
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb)
self:RenderRun()`), self:RenderRun()`),
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0)) method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey
local pop = _EntityService:GetEntityByPath(base) local pop = _EntityService:GetEntityByPath(base)
if pop == nil then if pop == nil then
return return
end end
self.DmgPopSeq = (self.DmgPopSeq or 0) + 1 self.DmgPopSeq = (self.DmgPopSeq or 0) + 1
local popSeq = self.DmgPopSeq local popSeq = self.DmgPopSeq
self:SetText(base, "") self:SetText(base, "")
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} } local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
local shown = tostring(math.max(0, math.floor(amount))) local shown = tostring(math.max(0, math.floor(amount)))
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS}) shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
end end
local digits = {} local digits = {}
for i = 1, string.len(shown) do for i = 1, string.len(shown) do
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0) table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
end end
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING} local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2 local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false) self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false)
end end
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitPath = base .. "/Digit" .. tostring(i) local digitPath = base .. "/Digit" .. tostring(i)
local digitEntity = _EntityService:GetEntityByPath(digitPath) local digitEntity = _EntityService:GetEntityByPath(digitPath)
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
if digits[i] ~= nil then if digits[i] ~= nil then
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1] digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
if digitEntity.UITransformComponent ~= nil then if digitEntity.UITransformComponent ~= nil then
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0) digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
end end
self:SetEntityEnabled(digitPath, true) self:SetEntityEnabled(digitPath, true)
else else
self:SetEntityEnabled(digitPath, false) self:SetEntityEnabled(digitPath, false)
end end
end end
end end
local popPos = nil local popPos = nil
local m = self.Monsters[slot] local m = self.Monsters[slot]
if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
local wp = m.entity.TransformComponent.WorldPosition local wp = m.entity.TransformComponent.WorldPosition
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45})) local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
popPos = _UILogic:ScreenToUIPosition(screen) popPos = _UILogic:ScreenToUIPosition(screen)
else else
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey) local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey)
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
local sp = slotEntity.UITransformComponent.anchoredPosition local sp = slotEntity.UITransformComponent.anchoredPosition
popPos = Vector2(sp.x, sp.y + 76) popPos = Vector2(sp.x, sp.y + 76)
end end
end end
if pop ~= nil and pop.UITransformComponent ~= nil then if pop ~= nil and pop.UITransformComponent ~= nil then
if popPos ~= nil then if popPos ~= nil then
pop.UITransformComponent.anchoredPosition = popPos pop.UITransformComponent.anchoredPosition = popPos
else else
pop.UITransformComponent.anchoredPosition = Vector2(0, 120) pop.UITransformComponent.anchoredPosition = Vector2(0, 120)
end end
end end
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
for i = 1, 6 do for i = 1, 6 do
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if self.DmgPopSeq ~= popSeq then if self.DmgPopSeq ~= popSeq then
return return
end end
local p = _EntityService:GetEntityByPath(base) local p = _EntityService:GetEntityByPath(base)
if p ~= nil and p.UITransformComponent ~= nil then if p ~= nil and p.UITransformComponent ~= nil then
local cur = p.UITransformComponent.anchoredPosition local cur = p.UITransformComponent.anchoredPosition
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7) p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
end end
end, 0.045 * i) end, 0.045 * i)
end end
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if self.DmgPopSeq ~= popSeq then if self.DmgPopSeq ~= popSeq then
return return
end end
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, false)
end, 0.48)`, [ end, 0.48)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
]), ]),
method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop" method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop"
if amount > 0 then if amount > 0 then
self:SetText(base, "-" .. string.format("%d", amount)) self:SetText(base, "-" .. string.format("%d", amount))
else else
self:SetText(base, "막음") self:SetText(base, "막음")
end end
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), _TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
if lp == nil then if lp == nil then
return return
end end
if lp.StateComponent == nil then if lp.StateComponent == nil then
return return
end end
pcall(function() lp.StateComponent:ChangeState("ATTACK") end) pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("IDLE") end) pcall(function() lp.StateComponent:ChangeState("IDLE") end)
end end
end, 0.5)`), end, 0.5)`),
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
if lp == nil then if lp == nil then
return return
end end
if lp.StateComponent ~= nil then if lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("HIT") end) pcall(function() lp.StateComponent:ChangeState("HIT") end)
end end
local tr = lp.TransformComponent local tr = lp.TransformComponent
if tr == nil then if tr == nil then
return return
end end
local p = tr.Position local p = tr.Position
tr.Position = Vector3(p.x - 0.15, p.y, p.z) tr.Position = Vector3(p.x - 0.15, p.y, p.z)
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z) lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end end
end, 0.15)`), end, 0.15)`),
method('MonsterLunge', `local m = self.Monsters[idx] method('MonsterLunge', `local m = self.Monsters[idx]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return return
end end
if m.motionBusy == true then if m.motionBusy == true then
return return
end end
m.motionBusy = true m.motionBusy = true
local e = m.entity local e = m.entity
local tr = e.TransformComponent local tr = e.TransformComponent
if tr == nil then if tr == nil then
m.motionBusy = false m.motionBusy = false
return return
end end
local p = tr.Position local p = tr.Position
tr.Position = Vector3(p.x - 0.35, p.y, p.z) tr.Position = Vector3(p.x - 0.35, p.y, p.z)
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x, p.y, p.z) e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end end
m.motionBusy = false m.motionBusy = false
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]), end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
method('MonsterHitMotion', `local m = self.Monsters[slot] method('MonsterHitMotion', `local m = self.Monsters[slot]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return return
end end
local e = m.entity local e = m.entity
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.hitClip e.SpriteRendererComponent.SpriteRUID = m.hitClip
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.standClip e.SpriteRendererComponent.SpriteRUID = m.standClip
end end
end, 0.5) end, 0.5)
else else
if m.motionBusy == true then if m.motionBusy == true then
return return
end end
m.motionBusy = true m.motionBusy = true
local tr = e.TransformComponent local tr = e.TransformComponent
if tr == nil then if tr == nil then
m.motionBusy = false m.motionBusy = false
return return
end end
local p = tr.Position local p = tr.Position
local seq = { 0.12, -0.12, 0 } local seq = { 0.12, -0.12, 0 }
for i = 1, #seq do for i = 1, #seq do
local dx = seq[i] local dx = seq[i]
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z) e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
end end
if i == #seq then if i == #seq then
m.motionBusy = false m.motionBusy = false
end end
end, 0.06 * i) end, 0.06 * i)
end end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path) method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
if e == nil or e.UITransformComponent == nil then if e == nil or e.UITransformComponent == nil then
return return
end end
local ratio = 0 local ratio = 0
if maxHp > 0 then ratio = hp / maxHp end if maxHp > 0 then ratio = hp / maxHp end
if ratio < 0 then ratio = 0 end if ratio < 0 then ratio = 0 end
local w = width * ratio local w = width * ratio
e.UITransformComponent.RectSize = Vector2(w, 14)`, [ e.UITransformComponent.RectSize = Vector2(w, 14)`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
]), ]),
method('PositionMonsterSlot', `local m = self.Monsters[slot] method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
if m == nil or m.entity == nil or not isvalid(m.entity) then self.TargetIndex = slot
return self:RenderCombat()
end end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
local tr = m.entity.TransformComponent method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
if tr == nil then if self.AscensionLevel > 0 then
return floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
end end
local wp = tr.WorldPosition self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Floor", floorText)
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y})) self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
local uipos = _UILogic:ScreenToUIPosition(screen)
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = uipos
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
self.TargetIndex = slot
self:RenderCombat()
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
if self.AscensionLevel > 0 then
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
end
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Floor", floorText)
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
];

View File

@@ -1,37 +1,24 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs'; import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.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'; 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 { 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 { 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';
export const runEndMethods = [ export const runEndMethods = [
method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} } method('ShowResult', `self:SetText("/ui/RunUIGroup/CombatHud/Result", text)
local target = maps[self.Floor] local entity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/Result")
if target == nil then if entity ~= nil then
return entity.Enable = true
end end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
local lp = _UserService.LocalPlayer method('EndRun', `local msg = text
if lp == nil then if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then
return self.AscensionUnlocked = self.AscensionUnlocked + 1
end local lp = _UserService.LocalPlayer
if lp.CurrentMapName == target then if lp ~= nil then
return self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId)
end end
_TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)`), self:RenderAscension()
method('ShowResult', `self:SetText("/ui/RunUIGroup/CombatHud/Result", text) msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!"
local entity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/Result") end
if entity ~= nil then self:ShowResult(msg)
entity.Enable = true self.RunActive = false
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), _TimerService:SetTimerOnce(function() self:ShowLobby() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
method('EndRun', `local msg = text
if text == "런 클리어!" and self.AscensionLevel >= self.AscensionUnlocked and self.AscensionUnlocked < 10 then
self.AscensionUnlocked = self.AscensionUnlocked + 1
local lp = _UserService.LocalPlayer
if lp ~= nil then
self:SaveAscension(self.AscensionUnlocked, lp.PlayerComponent.UserId)
end
self:RenderAscension()
msg = "런 클리어! 승천 " .. string.format("%d", self.AscensionUnlocked) .. " 해금!"
end
self:ShowResult(msg)
self.RunActive = false
_TimerService:SetTimerOnce(function() self:ShowLobby() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
];

View File

@@ -2,7 +2,7 @@ import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, A
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 { 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 { 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 { 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';
export const stateMethods = [ export const screensMethods = [
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false) method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false) self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false)
self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false) self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false)
@@ -21,14 +21,14 @@ self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud", false)
self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false) self:SetEntityEnabled("/ui/LobbyUIGroup/LobbyHud", false)
self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false) self:SetEntityEnabled("/ui/LobbyUIGroup/BoardHud", false)
self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)`), self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)`),
method('ActivateUIGroups', `local function grp(n) method('ActivateUIGroups', `local function enableGroup(name)
local g = _EntityService:GetEntityByPath("/ui/" .. n) local group = _EntityService:GetEntityByPath("/ui/" .. name)
if g ~= nil then g:SetEnable(true) end if group ~= nil then group:SetEnable(true) end
end end
grp("SelectUIGroup") enableGroup("SelectUIGroup")
grp("LobbyUIGroup") enableGroup("LobbyUIGroup")
grp("RunUIGroup") enableGroup("RunUIGroup")
grp("DeckUIGroup")`, [], 2), enableGroup("DeckUIGroup")`, [], 2),
method('ShowState', `self:HideGameHud() method('ShowState', `self:HideGameHud()
self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu") self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu")
self:SetEntityEnabled("/ui/SelectUIGroup/CharacterSelectHud", state == "charselect") self:SetEntityEnabled("/ui/SelectUIGroup/CharacterSelectHud", state == "charselect")
@@ -135,44 +135,17 @@ self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)
self:BindLobbyButtons() self:BindLobbyButtons()
self:BindMenuButtons() self:BindMenuButtons()
self:GoLobbyMap()`), self:GoLobbyMap()`),
method('GoLobbyMap', `self.LobbyTpTries = 0 method('RenderSoulLabel', `local soulPoints = self.SoulPoints or 0
local eventId = 0 self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", soulPoints))
local function go() self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", soulPoints))`),
self.LobbyTpTries = self.LobbyTpTries + 1
local lp = _UserService.LocalPlayer
if lp ~= nil then
if lp.CurrentMapName ~= "${LOBBY_MAP}" then
_TeleportService:TeleportToMapPosition(lp, ${LOBBY_SPAWN}, "${LOBBY_MAP}")
end
_TimerService:ClearTimer(eventId)
elseif self.LobbyTpTries > 50 then
_TimerService:ClearTimer(eventId)
end
end
eventId = _TimerService:SetTimerRepeat(go, 0.1)`),
method('OnLobbyNpcInteract', `if self.RunActive == true then
return
end
if id == "run" then
self:ShowCharacterSelect()
elseif id == "codex" then
self:ShowCodex()
elseif id == "shop" then
self:ShowSoulShop()
elseif id == "board" then
self:ShowBoard()
end`, [{ Type: 'string', DefaultValue: '""', SyncDirection: 0, Attributes: [], Name: 'id' }]),
method('RenderSoulLabel', `local s = self.SoulPoints or 0
self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s))
self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`),
method('BindLobbyButtons', `if self.LobbyBound == true then method('BindLobbyButtons', `if self.LobbyBound == true then
return return
end end
self.LobbyBound = true self.LobbyBound = true
local function bindClick(path, fn) local function bindClick(path, handler)
local e = _EntityService:GetEntityByPath(path) local entity = _EntityService:GetEntityByPath(path)
if e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then if entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent("ButtonComponent") ~= nil) then
e:ConnectEvent(ButtonClickEvent, fn) entity:ConnectEvent(ButtonClickEvent, handler)
end end
end end
bindClick("/ui/LobbyUIGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end) bindClick("/ui/LobbyUIGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end)

View File

@@ -3,7 +3,10 @@
import { POTIONS } from './lib/data.mjs'; import { POTIONS } from './lib/data.mjs';
import { prop, codeblock, RUN_LENGTH } from './lib/codeblock.mjs'; import { prop, codeblock, RUN_LENGTH } from './lib/codeblock.mjs';
import { bootMethods } from './cb/boot.mjs'; import { bootMethods } from './cb/boot.mjs';
import { stateMethods } from './cb/state.mjs'; import { screensMethods } from './cb/screens.mjs';
import { npcMethods } from './cb/npc.mjs';
import { navigationMethods } from './cb/navigation.mjs';
import { layoutMethods } from './cb/layout.mjs';
import { soulMethods } from './cb/soul.mjs'; import { soulMethods } from './cb/soul.mjs';
import { charSelectMethods } from './cb/charselect.mjs'; import { charSelectMethods } from './cb/charselect.mjs';
import { runMethods } from './cb/run.mjs'; import { runMethods } from './cb/run.mjs';
@@ -166,7 +169,9 @@ function writeCodeblocks() {
prop('any', 'NextTurnAddCards'), prop('any', 'NextTurnAddCards'),
], [ ], [
...bootMethods, ...bootMethods,
...stateMethods, ...screensMethods,
...npcMethods,
...navigationMethods,
...soulMethods, ...soulMethods,
...charSelectMethods, ...charSelectMethods,
...runMethods, ...runMethods,
@@ -177,6 +182,7 @@ function writeCodeblocks() {
...jobMethods, ...jobMethods,
...runEndMethods, ...runEndMethods,
...renderMethods, ...renderMethods,
...layoutMethods,
...rewardMethods, ...rewardMethods,
...itemMethods, ...itemMethods,
...tooltipMethods, ...tooltipMethods,

40
tools/verify/cbset.mjs Normal file
View File

@@ -0,0 +1,40 @@
// 순서 무관 codeblock 메서드 집합 비교. 본문 미출력 — 이름·차이 카운트만.
// 메서드 이동 리팩터의 무손실 검증용: 워킹트리 codeblock vs ref(기본 HEAD).
// 사용: node tools/verify/cbset.mjs [ref]
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
const PATH = 'RootDesk/MyDesk/SlayDeckController.codeblock';
const ref = process.argv[2] || 'HEAD';
function methodsOf(jsonText) {
const obj = JSON.parse(jsonText);
const arr = obj.ContentProto.Json.Methods;
const map = new Map();
for (const m of arr) {
map.set(m.Name, { code: m.Code, exec: m.ExecSpace, params: JSON.stringify(m.Parameters || []) });
}
return map;
}
const work = methodsOf(readFileSync(PATH, 'utf8'));
const base = methodsOf(execSync(`git show ${ref}:${PATH}`, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }));
const onlyWork = [...work.keys()].filter((k) => !base.has(k));
const onlyBase = [...base.keys()].filter((k) => !work.has(k));
const changed = [];
for (const k of work.keys()) {
if (!base.has(k)) continue;
const a = work.get(k);
const b = base.get(k);
if (a.code !== b.code || a.exec !== b.exec || a.params !== b.params) changed.push(k);
}
console.log(`ref=${ref} work=${work.size} base=${base.size}`);
console.log(`only-in-work (${onlyWork.length}): ${onlyWork.join(', ') || '-'}`);
console.log(`only-in-base (${onlyBase.length}): ${onlyBase.join(', ') || '-'}`);
console.log(`body/exec/params changed (${changed.length}): ${changed.join(', ') || '-'}`);
const ok = onlyWork.length === 0 && onlyBase.length === 0 && changed.length === 0;
console.log(ok ? 'RESULT: IDENTICAL SET (무손실)' : 'RESULT: DIFFERENCES ABOVE');
process.exit(ok ? 0 : 1);