diff --git a/RULES.md b/RULES.md index 7034356..181b3d4 100644 --- a/RULES.md +++ b/RULES.md @@ -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 시 토큰 폭발. - **게임 로직 수정** = `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/.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/.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 --`로 복원. - **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///...`)로 텍스트·이미지·표시숨김·상태기반 위치/크기/색을 **런타임 주입**(레이아웃=메이커, 내용=컨트롤러 — 메이커가 이 경로 유지 필수). 몬스터 슬롯 = `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은 오케스트레이터 잔류라 그쪽 변경은 자동 보존됨. @@ -65,6 +65,7 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock - 제목/본문은 UTF-8 spec JSON 파일로 작성 후 `create ` / `merge <번호>`. - PR 제목과 본문은 한국어로 작성한다. - 산출물 재생성 커밋은 소스 변경 커밋과 분리하거나, 메시지에 "산출물 재생성"을 명시. +- **PR 머지 후 브랜치 삭제**: 머지된 `feature/*`·`docs/*` 브랜치는 로컬·원격 모두 삭제한다. 삭제 전 `git merge-base --is-ancestor origin/<브랜치> origin/main`로 완전 머지 확인(종료코드 0=완전 머지 → 삭제 가능). main에 없는 커밋이 남은 브랜치와 `codex/*` 등 작업 중 브랜치는 보존한다. ## 5. 메이커(MSW) 연동 주의 @@ -86,3 +87,9 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock - UI 텍스트에서는 정수값인 숫자에 `.0`을 붙이지 않는다. `1.0/1.0`이 아니라 `1/1`처럼 표시한다. - 생성기 내 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 메서드를 작성하거나 기존 메서드를 손댈 때 이 규칙을 적용한다(대규모 일괄 개명은 별도 작업으로). diff --git a/RootDesk/MyDesk/SlayDeckController.codeblock b/RootDesk/MyDesk/SlayDeckController.codeblock index 952c981..863157c 100644 --- a/RootDesk/MyDesk/SlayDeckController.codeblock +++ b/RootDesk/MyDesk/SlayDeckController.codeblock @@ -1268,7 +1268,7 @@ "Name": null }, "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, "ExecSpace": 2, "Attributes": [], @@ -1351,45 +1351,7 @@ "Name": null }, "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)", - "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))", + "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": [], @@ -1404,7 +1366,7 @@ "Name": null }, "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, "ExecSpace": 6, "Attributes": [], @@ -1455,6 +1417,59 @@ "Attributes": [], "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": { "Type": "void", @@ -3937,21 +3952,6 @@ "Attributes": [], "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": { "Type": "void", @@ -4230,29 +4230,6 @@ "Attributes": [], "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": { "Type": "void", @@ -4291,6 +4268,29 @@ "Attributes": [], "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": { "Type": "any", diff --git a/tools/deck/cb/layout.mjs b/tools/deck/cb/layout.mjs new file mode 100644 index 0000000..dac0bbb --- /dev/null +++ b/tools/deck/cb/layout.mjs @@ -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' }]), +]; diff --git a/tools/deck/cb/navigation.mjs b/tools/deck/cb/navigation.mjs new file mode 100644 index 0000000..020d449 --- /dev/null +++ b/tools/deck/cb/navigation.mjs @@ -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)`), +]; diff --git a/tools/deck/cb/npc.mjs b/tools/deck/cb/npc.mjs new file mode 100644 index 0000000..73815fe --- /dev/null +++ b/tools/deck/cb/npc.mjs @@ -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' }]), +]; diff --git a/tools/deck/cb/render.mjs b/tools/deck/cb/render.mjs index d89883e..cff777d 100644 --- a/tools/deck/cb/render.mjs +++ b/tools/deck/cb/render.mjs @@ -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 { 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 renderMethods = [ - method('BuffsLabel', `local parts = {} -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 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 -return table.concat(parts, " ")`, [ - { 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: 'vuln' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' }, - ], 0, 'string'), - method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do - local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) - local m = self.Monsters[i] - if m ~= nil and m.alive == true then - self:SetEntityEnabled(base, true) - self:SetText(base .. "/Name", m.name) - self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp)) - local intent = m.intents[m.intentIdx] - local t = "" - if intent ~= nil then - if intent.kind == "Attack" then - local atk = intent.value + m.str - if m.weak > 0 then atk = math.floor(atk * 0.75) end - if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end - t = "공격 " .. tostring(atk) - elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value) - elseif intent.kind == "Debuff" then - if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여" - else t = "취약 " .. tostring(intent.value) .. " 부여" end - elseif intent.kind == "AddCard" then - t = "저주 카드 추가" - end - end - self:SetText(base .. "/Intent", t) - local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 - local shownTarget = self.TargetIndex - if dragActive == true then shownTarget = self.DragTargetIndex end - self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive) - self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive) - local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent") - if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then - if intent.kind == "Attack" then - intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1) - elseif intent.kind == "Debuff" then - intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1) - elseif intent.kind == "AddCard" then - intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1) - else - intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1) - end - end - self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W}) - self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0) - 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)) - else - self:SetEntityEnabled(base, false) - end -end -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:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) -self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) -local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) -if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then - if pb ~= "" then pb = pb .. " " end - pb = pb .. "불가침" .. tostring(self.PlayerIntangible) -end -if self.PlayerDex ~= nil and self.PlayerDex > 0 then - if pb ~= "" then pb = pb .. " " end - pb = pb .. "민첩+" .. tostring(self.PlayerDex) -end -if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then - if pb ~= "" then pb = pb .. " " end - pb = pb .. "가시" .. tostring(self.PlayerThorns) -end -if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then - local names = {} - for i = 1, #self.PlayerPowers do - local pc = self.Cards[self.PlayerPowers[i]] - if pc ~= nil then table.insert(names, pc.name) end - end - if pb ~= "" then pb = pb .. " · " end - pb = pb .. table.concat(names, " ") -end -self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb) -self:RenderRun()`), - method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0)) -local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey -local pop = _EntityService:GetEntityByPath(base) -if pop == nil then - return -end -self.DmgPopSeq = (self.DmgPopSeq or 0) + 1 -local popSeq = self.DmgPopSeq -self:SetText(base, "") -local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} } -local shown = tostring(math.max(0, math.floor(amount))) -if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then - shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS}) -end -local digits = {} -for i = 1, string.len(shown) do - table.insert(digits, tonumber(string.sub(shown, i, i)) or 0) -end -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 -for i = 1, ${DAMAGE_POP_MAX_DIGITS} do - self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false) -end -for i = 1, ${DAMAGE_POP_MAX_DIGITS} do - local digitPath = base .. "/Digit" .. tostring(i) - local digitEntity = _EntityService:GetEntityByPath(digitPath) - if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then - if digits[i] ~= nil then - digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1] - digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) - if digitEntity.UITransformComponent ~= nil then - digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0) - end - self:SetEntityEnabled(digitPath, true) - else - self:SetEntityEnabled(digitPath, false) - end - end -end -local popPos = nil -local m = self.Monsters[slot] -if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then - local wp = m.entity.TransformComponent.WorldPosition - local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45})) - popPos = _UILogic:ScreenToUIPosition(screen) -else - local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey) - if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then - local sp = slotEntity.UITransformComponent.anchoredPosition - popPos = Vector2(sp.x, sp.y + 76) - end -end -if pop ~= nil and pop.UITransformComponent ~= nil then - if popPos ~= nil then - pop.UITransformComponent.anchoredPosition = popPos - else - pop.UITransformComponent.anchoredPosition = Vector2(0, 120) - end -end -self:SetEntityEnabled(base, true) -for i = 1, 6 do - _TimerService:SetTimerOnce(function() - if self.DmgPopSeq ~= popSeq then - return - end - local p = _EntityService:GetEntityByPath(base) - if p ~= nil and p.UITransformComponent ~= nil then - local cur = p.UITransformComponent.anchoredPosition - p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7) - end - end, 0.045 * i) -end -_TimerService:SetTimerOnce(function() - if self.DmgPopSeq ~= popSeq then - return - end - self:SetEntityEnabled(base, false) -end, 0.48)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, - ]), - method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop" -if amount > 0 then - self:SetText(base, "-" .. string.format("%d", amount)) -else - self:SetText(base, "막음") -end -self:SetEntityEnabled(base, true) -_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 -if lp == nil then - return -end -if lp.StateComponent == nil then - return -end -pcall(function() lp.StateComponent:ChangeState("ATTACK") end) -_TimerService:SetTimerOnce(function() - if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then - pcall(function() lp.StateComponent:ChangeState("IDLE") end) - end -end, 0.5)`), - method('PlayerHitMotion', `local lp = _UserService.LocalPlayer -if lp == nil then - return -end -if lp.StateComponent ~= nil then - pcall(function() lp.StateComponent:ChangeState("HIT") end) -end -local tr = lp.TransformComponent -if tr == nil then - return -end -local p = tr.Position -tr.Position = Vector3(p.x - 0.15, p.y, p.z) -_TimerService:SetTimerOnce(function() - if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then - lp.TransformComponent.Position = Vector3(p.x, p.y, p.z) - end -end, 0.15)`), - method('MonsterLunge', `local m = self.Monsters[idx] -if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then - return -end -if m.motionBusy == true then - return -end -m.motionBusy = true -local e = m.entity -local tr = e.TransformComponent -if tr == nil then - m.motionBusy = false - return -end -local p = tr.Position -tr.Position = Vector3(p.x - 0.35, p.y, p.z) -_TimerService:SetTimerOnce(function() - if isvalid(e) and e.TransformComponent ~= nil then - e.TransformComponent.Position = Vector3(p.x, p.y, p.z) - end - m.motionBusy = false -end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]), - method('MonsterHitMotion', `local m = self.Monsters[slot] -if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then - return -end -local e = m.entity -if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then - e.SpriteRendererComponent.SpriteRUID = m.hitClip - _TimerService:SetTimerOnce(function() - if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then - e.SpriteRendererComponent.SpriteRUID = m.standClip - end - end, 0.5) -else - if m.motionBusy == true then - return - end - m.motionBusy = true - local tr = e.TransformComponent - if tr == nil then - m.motionBusy = false - return - end - local p = tr.Position - local seq = { 0.12, -0.12, 0 } - for i = 1, #seq do - local dx = seq[i] - _TimerService:SetTimerOnce(function() - if isvalid(e) and e.TransformComponent ~= nil then - e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z) - end - if i == #seq then - m.motionBusy = false - end - end, 0.06 * i) - end -end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('SetHpBar', `local e = _EntityService:GetEntityByPath(path) -if e == nil or e.UITransformComponent == nil then - return -end -local ratio = 0 -if maxHp > 0 then ratio = hp / maxHp end -if ratio < 0 then ratio = 0 end -local w = width * ratio -e.UITransformComponent.RectSize = Vector2(w, 14)`, [ - { 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: 'maxHp' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' }, - ]), - method('PositionMonsterSlot', `local m = self.Monsters[slot] -if m == nil or m.entity == nil or not isvalid(m.entity) then - return -end -local tr = m.entity.TransformComponent -if tr == nil then - return -end -local wp = tr.WorldPosition -local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y})) -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))`), -]; +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 renderMethods = [ + method('BuffsLabel', `local parts = {} +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 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 +return table.concat(parts, " ")`, [ + { 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: 'vuln' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' }, + ], 0, 'string'), + method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do + local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) + local m = self.Monsters[i] + if m ~= nil and m.alive == true then + self:SetEntityEnabled(base, true) + self:SetText(base .. "/Name", m.name) + self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp)) + local intent = m.intents[m.intentIdx] + local t = "" + if intent ~= nil then + if intent.kind == "Attack" then + local atk = intent.value + m.str + if m.weak > 0 then atk = math.floor(atk * 0.75) end + if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end + t = "공격 " .. tostring(atk) + elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value) + elseif intent.kind == "Debuff" then + if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여" + else t = "취약 " .. tostring(intent.value) .. " 부여" end + elseif intent.kind == "AddCard" then + t = "저주 카드 추가" + end + end + self:SetText(base .. "/Intent", t) + local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 + local shownTarget = self.TargetIndex + if dragActive == true then shownTarget = self.DragTargetIndex end + self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive) + self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive) + local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent") + if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then + if intent.kind == "Attack" then + intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1) + elseif intent.kind == "Debuff" then + intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1) + elseif intent.kind == "AddCard" then + intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1) + else + intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1) + end + end + self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W}) + self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0) + 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)) + else + self:SetEntityEnabled(base, false) + end +end +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:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) +self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) +local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) +if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then + if pb ~= "" then pb = pb .. " " end + pb = pb .. "불가침" .. tostring(self.PlayerIntangible) +end +if self.PlayerDex ~= nil and self.PlayerDex > 0 then + if pb ~= "" then pb = pb .. " " end + pb = pb .. "민첩+" .. tostring(self.PlayerDex) +end +if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then + if pb ~= "" then pb = pb .. " " end + pb = pb .. "가시" .. tostring(self.PlayerThorns) +end +if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then + local names = {} + for i = 1, #self.PlayerPowers do + local pc = self.Cards[self.PlayerPowers[i]] + if pc ~= nil then table.insert(names, pc.name) end + end + if pb ~= "" then pb = pb .. " · " end + pb = pb .. table.concat(names, " ") +end +self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb) +self:RenderRun()`), + method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0)) +local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey +local pop = _EntityService:GetEntityByPath(base) +if pop == nil then + return +end +self.DmgPopSeq = (self.DmgPopSeq or 0) + 1 +local popSeq = self.DmgPopSeq +self:SetText(base, "") +local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} } +local shown = tostring(math.max(0, math.floor(amount))) +if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then + shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS}) +end +local digits = {} +for i = 1, string.len(shown) do + table.insert(digits, tonumber(string.sub(shown, i, i)) or 0) +end +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 +for i = 1, ${DAMAGE_POP_MAX_DIGITS} do + self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false) +end +for i = 1, ${DAMAGE_POP_MAX_DIGITS} do + local digitPath = base .. "/Digit" .. tostring(i) + local digitEntity = _EntityService:GetEntityByPath(digitPath) + if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then + if digits[i] ~= nil then + digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1] + digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) + if digitEntity.UITransformComponent ~= nil then + digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0) + end + self:SetEntityEnabled(digitPath, true) + else + self:SetEntityEnabled(digitPath, false) + end + end +end +local popPos = nil +local m = self.Monsters[slot] +if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then + local wp = m.entity.TransformComponent.WorldPosition + local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45})) + popPos = _UILogic:ScreenToUIPosition(screen) +else + local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey) + if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then + local sp = slotEntity.UITransformComponent.anchoredPosition + popPos = Vector2(sp.x, sp.y + 76) + end +end +if pop ~= nil and pop.UITransformComponent ~= nil then + if popPos ~= nil then + pop.UITransformComponent.anchoredPosition = popPos + else + pop.UITransformComponent.anchoredPosition = Vector2(0, 120) + end +end +self:SetEntityEnabled(base, true) +for i = 1, 6 do + _TimerService:SetTimerOnce(function() + if self.DmgPopSeq ~= popSeq then + return + end + local p = _EntityService:GetEntityByPath(base) + if p ~= nil and p.UITransformComponent ~= nil then + local cur = p.UITransformComponent.anchoredPosition + p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7) + end + end, 0.045 * i) +end +_TimerService:SetTimerOnce(function() + if self.DmgPopSeq ~= popSeq then + return + end + self:SetEntityEnabled(base, false) +end, 0.48)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, + ]), + method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop" +if amount > 0 then + self:SetText(base, "-" .. string.format("%d", amount)) +else + self:SetText(base, "막음") +end +self:SetEntityEnabled(base, true) +_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 +if lp == nil then + return +end +if lp.StateComponent == nil then + return +end +pcall(function() lp.StateComponent:ChangeState("ATTACK") end) +_TimerService:SetTimerOnce(function() + if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then + pcall(function() lp.StateComponent:ChangeState("IDLE") end) + end +end, 0.5)`), + method('PlayerHitMotion', `local lp = _UserService.LocalPlayer +if lp == nil then + return +end +if lp.StateComponent ~= nil then + pcall(function() lp.StateComponent:ChangeState("HIT") end) +end +local tr = lp.TransformComponent +if tr == nil then + return +end +local p = tr.Position +tr.Position = Vector3(p.x - 0.15, p.y, p.z) +_TimerService:SetTimerOnce(function() + if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then + lp.TransformComponent.Position = Vector3(p.x, p.y, p.z) + end +end, 0.15)`), + method('MonsterLunge', `local m = self.Monsters[idx] +if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then + return +end +if m.motionBusy == true then + return +end +m.motionBusy = true +local e = m.entity +local tr = e.TransformComponent +if tr == nil then + m.motionBusy = false + return +end +local p = tr.Position +tr.Position = Vector3(p.x - 0.35, p.y, p.z) +_TimerService:SetTimerOnce(function() + if isvalid(e) and e.TransformComponent ~= nil then + e.TransformComponent.Position = Vector3(p.x, p.y, p.z) + end + m.motionBusy = false +end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]), + method('MonsterHitMotion', `local m = self.Monsters[slot] +if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then + return +end +local e = m.entity +if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then + e.SpriteRendererComponent.SpriteRUID = m.hitClip + _TimerService:SetTimerOnce(function() + if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then + e.SpriteRendererComponent.SpriteRUID = m.standClip + end + end, 0.5) +else + if m.motionBusy == true then + return + end + m.motionBusy = true + local tr = e.TransformComponent + if tr == nil then + m.motionBusy = false + return + end + local p = tr.Position + local seq = { 0.12, -0.12, 0 } + for i = 1, #seq do + local dx = seq[i] + _TimerService:SetTimerOnce(function() + if isvalid(e) and e.TransformComponent ~= nil then + e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z) + end + if i == #seq then + m.motionBusy = false + end + end, 0.06 * i) + end +end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('SetHpBar', `local e = _EntityService:GetEntityByPath(path) +if e == nil or e.UITransformComponent == nil then + return +end +local ratio = 0 +if maxHp > 0 then ratio = hp / maxHp end +if ratio < 0 then ratio = 0 end +local w = width * ratio +e.UITransformComponent.RectSize = Vector2(w, 14)`, [ + { 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: 'maxHp' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' }, + ]), + 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))`), +]; diff --git a/tools/deck/cb/runend.mjs b/tools/deck/cb/runend.mjs index 8749d3c..f70eecb 100644 --- a/tools/deck/cb/runend.mjs +++ b/tools/deck/cb/runend.mjs @@ -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 { 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 runEndMethods = [ - method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} } -local target = maps[self.Floor] -if target == nil then - return -end -local lp = _UserService.LocalPlayer -if lp == nil then - return -end -if lp.CurrentMapName == target then - return -end -_TeleportService:TeleportToMapPosition(lp, Vector3(-6, 0.03, 0), target)`), - method('ShowResult', `self:SetText("/ui/RunUIGroup/CombatHud/Result", text) -local entity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/Result") -if entity ~= nil then - entity.Enable = true -end`, [{ 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' }]), -]; +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 runEndMethods = [ + method('ShowResult', `self:SetText("/ui/RunUIGroup/CombatHud/Result", text) +local entity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/Result") +if entity ~= nil then + entity.Enable = true +end`, [{ 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' }]), +]; diff --git a/tools/deck/cb/state.mjs b/tools/deck/cb/screens.mjs similarity index 86% rename from tools/deck/cb/state.mjs rename to tools/deck/cb/screens.mjs index 8e260db..7fe661c 100644 --- a/tools/deck/cb/state.mjs +++ b/tools/deck/cb/screens.mjs @@ -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 { 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) self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", 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/BoardHud", false) self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false)`), - method('ActivateUIGroups', `local function grp(n) - local g = _EntityService:GetEntityByPath("/ui/" .. n) - if g ~= nil then g:SetEnable(true) end + method('ActivateUIGroups', `local function enableGroup(name) + local group = _EntityService:GetEntityByPath("/ui/" .. name) + if group ~= nil then group:SetEnable(true) end end -grp("SelectUIGroup") -grp("LobbyUIGroup") -grp("RunUIGroup") -grp("DeckUIGroup")`, [], 2), +enableGroup("SelectUIGroup") +enableGroup("LobbyUIGroup") +enableGroup("RunUIGroup") +enableGroup("DeckUIGroup")`, [], 2), method('ShowState', `self:HideGameHud() self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu") self:SetEntityEnabled("/ui/SelectUIGroup/CharacterSelectHud", state == "charselect") @@ -135,44 +135,17 @@ self:SetEntityEnabled("/ui/LobbyUIGroup/SoulShopHud", false) self:BindLobbyButtons() self:BindMenuButtons() self:GoLobbyMap()`), - method('GoLobbyMap', `self.LobbyTpTries = 0 -local eventId = 0 -local function go() - 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('RenderSoulLabel', `local soulPoints = self.SoulPoints or 0 +self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", soulPoints)) +self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", soulPoints))`), method('BindLobbyButtons', `if self.LobbyBound == true then return end self.LobbyBound = true -local function bindClick(path, fn) - local e = _EntityService:GetEntityByPath(path) - if e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then - e:ConnectEvent(ButtonClickEvent, fn) +local function bindClick(path, handler) + local entity = _EntityService:GetEntityByPath(path) + if entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent("ButtonComponent") ~= nil) then + entity:ConnectEvent(ButtonClickEvent, handler) end end bindClick("/ui/LobbyUIGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end) diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 4c3ceac..3da87e7 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -3,7 +3,10 @@ import { POTIONS } from './lib/data.mjs'; import { prop, codeblock, RUN_LENGTH } from './lib/codeblock.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 { charSelectMethods } from './cb/charselect.mjs'; import { runMethods } from './cb/run.mjs'; @@ -166,7 +169,9 @@ function writeCodeblocks() { prop('any', 'NextTurnAddCards'), ], [ ...bootMethods, - ...stateMethods, + ...screensMethods, + ...npcMethods, + ...navigationMethods, ...soulMethods, ...charSelectMethods, ...runMethods, @@ -177,6 +182,7 @@ function writeCodeblocks() { ...jobMethods, ...runEndMethods, ...renderMethods, + ...layoutMethods, ...rewardMethods, ...itemMethods, ...tooltipMethods, diff --git a/tools/verify/cbset.mjs b/tools/verify/cbset.mjs new file mode 100644 index 0000000..b651367 --- /dev/null +++ b/tools/verify/cbset.mjs @@ -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);