From a14193967546d4d634f8425034c2fc7b89d4e228 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 16 Jun 2026 08:02:22 +0900 Subject: [PATCH] =?UTF-8?q?refactor(cb):=20codeblock=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20161=EA=B0=9C=EB=A5=BC=20cb/*.mjs=2017=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(codeblock=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EB=8F=99=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit writeCodeblocks의 메서드를 연속-런 17 모듈(boot/state/soul/charselect/run/ deckturn/deckview/hand/combat/jobs/runend/render/reward/items/tooltip/map/shop)로 분리, methods 배열은 spread-concat(원본 순서 보존). prop 103개는 오케스트레이터 유지. 산출물 무변경(diffcheck: SlayDeckController.codeblock IDENTICAL). Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/deck/cb/boot.mjs | 73 + tools/deck/cb/charselect.mjs | 58 + tools/deck/cb/combat.mjs | 480 +++++ tools/deck/cb/deckturn.mjs | 344 ++++ tools/deck/cb/deckview.mjs | 229 +++ tools/deck/cb/hand.mjs | 401 +++++ tools/deck/cb/items.mjs | 213 +++ tools/deck/cb/jobs.mjs | 79 + tools/deck/cb/map.mjs | 230 +++ tools/deck/cb/render.mjs | 308 ++++ tools/deck/cb/reward.mjs | 55 + tools/deck/cb/run.mjs | 207 +++ tools/deck/cb/runend.mjs | 37 + tools/deck/cb/shop.mjs | 173 ++ tools/deck/cb/soul.mjs | 114 ++ tools/deck/cb/state.mjs | 193 ++ tools/deck/cb/tooltip.mjs | 115 ++ tools/deck/gen-slaydeck.mjs | 3241 +--------------------------------- 18 files changed, 3343 insertions(+), 3207 deletions(-) create mode 100644 tools/deck/cb/boot.mjs create mode 100644 tools/deck/cb/charselect.mjs create mode 100644 tools/deck/cb/combat.mjs create mode 100644 tools/deck/cb/deckturn.mjs create mode 100644 tools/deck/cb/deckview.mjs create mode 100644 tools/deck/cb/hand.mjs create mode 100644 tools/deck/cb/items.mjs create mode 100644 tools/deck/cb/jobs.mjs create mode 100644 tools/deck/cb/map.mjs create mode 100644 tools/deck/cb/render.mjs create mode 100644 tools/deck/cb/reward.mjs create mode 100644 tools/deck/cb/run.mjs create mode 100644 tools/deck/cb/runend.mjs create mode 100644 tools/deck/cb/shop.mjs create mode 100644 tools/deck/cb/soul.mjs create mode 100644 tools/deck/cb/state.mjs create mode 100644 tools/deck/cb/tooltip.mjs diff --git a/tools/deck/cb/boot.mjs b/tools/deck/cb/boot.mjs new file mode 100644 index 0000000..b7e80de --- /dev/null +++ b/tools/deck/cb/boot.mjs @@ -0,0 +1,73 @@ +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 bootMethods = [ + method('OnBeginPlay', `${luaCardsTable(CARDS.cards)} +${luaFramesTable()} +${luaNodeIconsTable()} +${luaSoulShopTable(SOUL_UNLOCKS)} +self.SoulUnlocks = {} +self.SoulPoints = self.SoulPoints or 0 +self:ShowLobby() +local lp = _UserService.LocalPlayer +if lp ~= nil then + self:ReqLoadAscension(lp.PlayerComponent.UserId) + self:ReqLoadSouls(lp.PlayerComponent.UserId) +end +_InputService:ConnectEvent(KeyDownEvent, function(e) + if e.key == KeyboardKey.LeftControl then + local lp2 = _UserService.LocalPlayer + if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then + self:PlayerAttackMotion() + end + end +end)`), + method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId) +local errCode, value = ds:GetAndWait("ascensionUnlocked") +local n = 0 +if errCode == 0 and value ~= nil and value ~= "" then + n = tonumber(value) or 0 +end +self:RecvAscension(n, userId)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }], 5), + method('RecvAscension', `self.AscensionUnlocked = n +if self.AscensionLevel > self.AscensionUnlocked then + self.AscensionLevel = self.AscensionUnlocked +end +self:RenderAscension()`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }, + ], 6), + method('SaveAscension', `local ds = _DataStorageService:GetUserDataStorage(userId) +ds:SetAndWait("ascensionUnlocked", tostring(n))`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }, + ], 5), + method('AdjustAscension', `local v = self.AscensionLevel + delta +if v < 0 then v = 0 end +if v > self.AscensionUnlocked then v = self.AscensionUnlocked end +self.AscensionLevel = v +self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]), + method('RenderAscension', `self:SetText("/ui/DefaultGroup/MainMenu/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked)) +self:SetText("/ui/DefaultGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`), + method('AscHpMult', `local m = 1 +if self.AscensionLevel >= 1 then m = m + 0.1 end +if self.AscensionLevel >= 6 then m = m + 0.1 end +return m`, [], 0, 'number'), + method('AscAtkMult', `local m = 1 +if self.AscensionLevel >= 2 then m = m + 0.1 end +if self.AscensionLevel >= 7 then m = m + 0.1 end +return m`, [], 0, 'number'), + method('AscEliteBonus', `local b = 0 +if self.AscensionLevel >= 4 then b = b + 0.2 end +if self.AscensionLevel >= 9 then b = b + 0.2 end +return b`, [], 0, 'number'), + method('AscGoldMult', `local m = 1 +if self.AscensionLevel >= 5 then m = m - 0.25 end +if self.AscensionLevel >= 10 then m = m - 0.25 end +return m`, [], 0, 'number'), + method('AscStartHpPenalty', `local p = 0 +if self.AscensionLevel >= 3 then p = p + 10 end +if self.AscensionLevel >= 8 then p = p + 10 end +return p`, [], 0, 'number'), +]; diff --git a/tools/deck/cb/charselect.mjs b/tools/deck/cb/charselect.mjs new file mode 100644 index 0000000..3a8299e --- /dev/null +++ b/tools/deck/cb/charselect.mjs @@ -0,0 +1,58 @@ +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 charSelectMethods = [ + method('ShowCharacterSelect', `self.SelectedClass = "" +self:ShowState("charselect") +self:RenderCharacterSelect()`), + method('SelectClass', `self.SelectedClass = className +self:RenderCharacterSelect()`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }, + ]), + method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton") +if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then + if self.SelectedClass == "warrior" then + warrior.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) + else + warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) + end +end +local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton") +if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then + if self.SelectedClass == "magician" then + mage.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) + else + mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) + end +end +local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton") +if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then + if self.SelectedClass == "bandit" then + thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) + else + thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) + end +end +if self.SelectedClass == "warrior" then + self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨") +elseif self.SelectedClass == "bandit" then + self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "도적 선택됨") +elseif self.SelectedClass == "magician" then + self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨") +else + self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요") +end`), + method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then + self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요") + return +end +self:StartRun()`), + method('SetEntityEnabled', `local e = _EntityService:GetEntityByPath(path) +if e ~= nil then + e.Enable = enabled +end`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' }, + ]), +]; diff --git a/tools/deck/cb/combat.mjs b/tools/deck/cb/combat.mjs new file mode 100644 index 0000000..56a58d3 --- /dev/null +++ b/tools/deck/cb/combat.mjs @@ -0,0 +1,480 @@ +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 combatMethods = [ + method('PlayCard', `if self:IsDiscardSelecting() == true then + self:SelectDiscardSlot(slot) + return +end +if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then + return +end +if self.Hand == nil then + return +end +local cardId = self.Hand[slot] +if cardId == nil then + return +end +local c = self.Cards[cardId] +if c == nil then + return +end +if c.unplayable == true then + self:Toast("사용할 수 없는 카드입니다") + return +end +if self.Energy < c.cost then + self:Toast("에너지가 부족합니다") + return +end +self.Energy = self.Energy - c.cost +self:ResolveCardEffects(cardId, c, false) +table.remove(self.Hand, slot) +if c.exhaust == true then + if self.ExhaustPile == nil then self.ExhaustPile = {} end + table.insert(self.ExhaustPile, cardId) +elseif c.kind ~= "Power" then + table.insert(self.DiscardPile, cardId) +end +self:RenderHand(false) +self:RenderPiles() +self:RenderCombat() +if self:BeginDiscardSelection(c) == true then + return +end +self:RenderHand(false) +self:RenderPiles() +self:RenderCombat() +self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('OnCardButton', `if self:IsDiscardSelecting() == true then + self:SelectDiscardSlot(slot) +end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('FindMonsterAtTouch', `local best = 0 +local bestDist = 200 +for i = 1, #self.Monsters do + local m = self.Monsters[i] + if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then + local wp = m.entity.TransformComponent.WorldPosition + local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7)) + local dx = sp.x - touchPoint.x + local dy = sp.y - touchPoint.y + local d = math.sqrt(dx * dx + dy * dy) + if d < bestDist then + bestDist = d + best = i + end + end +end +return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'), + method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 +local shownTarget = self.TargetIndex +if dragActive == true then shownTarget = self.DragTargetIndex end +for i = 1, #self.Monsters do + local m = self.Monsters[i] + local active = false + if m ~= nil and m.alive == true and i == shownTarget then active = true end + self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active) + self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive) + self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive) +end`), + method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then + return +end +if self.Hand == nil or self.Hand[slot] == nil then + return +end +if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then + _TimerService:ClearTimer(self.CardHoverTweenId) + self.CardHoverTweenId = 0 +end +for i = 1, 10 do + local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) + if e ~= nil and e.UITransformComponent ~= nil then + e.UITransformComponent.UIScale = Vector3(1, 1, 1) + e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0) + end +end +self.DragSlot = slot +self.DragTargetIndex = 0 +self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('OnCardDrag', `if self.DragSlot ~= slot then + return +end +local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) +if e ~= nil and e.UITransformComponent ~= nil then + local ui = _UILogic:ScreenToUIPosition(touchPoint) + e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360) +end +local cardId = self.Hand[slot] +local c = nil +if cardId ~= nil then c = self.Cards[cardId] end +if c ~= nil and c.kind == "Attack" then + local best = self:FindMonsterAtTouch(touchPoint) + if best ~= self.DragTargetIndex then + self.DragTargetIndex = best + self:RenderTargetFrames() + end +end`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, + ]), + method('OnCardDragEnd', `if self.DragSlot ~= slot then + return +end +self.DragSlot = 0 +local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) +if e ~= nil and e.UITransformComponent ~= nil then + e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0) + e.UITransformComponent.UIScale = Vector3(1, 1, 1) +end +self:ResolveCardDrop(slot, touchPoint)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, + ]), + method('ResolveCardDrop', `if self:IsDiscardSelecting() == true then + self:SelectDiscardSlot(slot) + return +end +if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then + return +end +local cardId = self.Hand[slot] +if cardId == nil then + return +end +local c = self.Cards[cardId] +if c == nil then + return +end +if c.kind == "Attack" then + local best = self.DragTargetIndex or 0 + if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end + self.DragTargetIndex = 0 + if best > 0 then + self.TargetIndex = best + self:PlayCard(slot) + self:RenderTargetFrames() + else + self:RenderTargetFrames() + end +else + self.DragTargetIndex = 0 + self:RenderTargetFrames() + local ui = _UILogic:ScreenToUIPosition(touchPoint) + if ui.y > -180 then + self:PlayCard(slot) + end +end`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, + ]), + method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]), + method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex] +if m == nil or m.alive ~= true then + m = nil + for i = 1, #self.Monsters do + if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end + end +end +if m == nil then + return +end +local dmg = amount +if m.vuln > 0 then + dmg = math.floor(dmg * 1.5) +end +if m.block > 0 and pierce ~= true then + local absorbed = math.min(m.block, dmg) + m.block = m.block - absorbed + dmg = dmg - absorbed +end +m.hp = m.hp - dmg +self:MonsterHitMotion(m.slot) +if m.hp <= 0 then + m.hp = 0 + self:KillMonster(m.slot) +end`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, + ]), + method('PlayAttackFx', `local m = self.Monsters[targetIndex] +if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then + self:DealDamageToTarget(damage, pierce) + self:RenderCombat() + self:CheckCombatEnd() + return +end +self.FxBusy = true +local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx") +if fx ~= nil then + if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then + fx.SpriteGUIRendererComponent.ImageRUID = image + end + if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then + local wp = m.entity.TransformComponent.WorldPosition + local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7)) + fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp) + end + fx.Enable = true +end +_TimerService:SetTimerOnce(function() + if fx ~= nil then fx.Enable = false end + self.FxBusy = false + local shown = damage + local mt = self.Monsters[targetIndex] + if mt ~= nil and mt.alive == true and mt.vuln > 0 then + shown = math.floor(damage * 1.5) + end + self:DealDamageToTarget(damage, pierce) + self:ShowDmgPop(targetIndex, shown) + self:RenderCombat() + self:CheckCombatEnd() +end, 0.35)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, + ]), + method('PlayAoeFx', `self.FxBusy = true +local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx") +if fx ~= nil then + if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then + fx.SpriteGUIRendererComponent.ImageRUID = image + end + if fx.UITransformComponent ~= nil then + fx.UITransformComponent.anchoredPosition = Vector2(300, 60) + end + fx.Enable = true +end +_TimerService:SetTimerOnce(function() + if fx ~= nil then fx.Enable = false end + self.FxBusy = false + for i = 1, #self.Monsters do + local m = self.Monsters[i] + if m ~= nil and m.alive == true then + local dmg = damage + if m.vuln > 0 then + dmg = math.floor(dmg * 1.5) + end + if m.block > 0 then + local absorbed = math.min(m.block, dmg) + m.block = m.block - absorbed + dmg = dmg - absorbed + end + m.hp = m.hp - dmg + self:ShowDmgPop(i, dmg) + self:MonsterHitMotion(i) + if m.hp <= 0 then + m.hp = 0 + self:KillMonster(m.slot) + end + end + end + self:RenderCombat() + self:CheckCombatEnd() +end, 0.35)`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, + ]), + method('KillMonster', `local m = self.Monsters[slot] +if m == nil then + return +end +m.alive = false +if m.entity ~= nil and isvalid(m.entity) then + local ent = m.entity + _TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4) +end +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false) +for i = 1, #self.Monsters do + if self.Monsters[i].alive == true then self.TargetIndex = i; break end +end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('DealDamageToPlayer', `local dmg = amount +if self.PlayerBlock > 0 then + local absorbed = math.min(self.PlayerBlock, dmg) + self.PlayerBlock = self.PlayerBlock - absorbed + dmg = dmg - absorbed +end +if dmg > 0 then + self.PlayerHp = self.PlayerHp - dmg + local reflect = self.PlayerThorns or 0 + if self:HasRelic("bronzeScales") then + reflect = reflect + 3 + end + if reflect > 0 and attackerSlot ~= nil and attackerSlot > 0 then + local am = self.Monsters[attackerSlot] + if am ~= nil and am.alive == true then + am.hp = am.hp - reflect + self:ShowDmgPop(am.slot, reflect) + self:MonsterHitMotion(am.slot) + if am.hp <= 0 then + am.hp = 0 + self:KillMonster(am.slot) + end + end + end + if self:HasRelic("selfFormingClay") then + self.ClayBlockNext = self.ClayBlockNext + 3 + end + if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then + self.FirstHpLossDone = true + self:DrawCards(3) + self:RenderHand(false) + end +end +if self.PlayerHp < 0 then + self.PlayerHp = 0 +end`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' }, + ]), + method('EnemyTurn', `self.TurnBusy = true +self:EnemyActStep(1)`), + method('EnemyActStep', `local idx = 0 +for i = fromIndex, #self.Monsters do + if self.Monsters[i].alive == true then idx = i; break end +end +if idx == 0 or self.PlayerHp <= 0 then + self:FinishEnemyTurn() + return +end +local m = self.Monsters[idx] +local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx) +self:SetEntityEnabled(base .. "/ActFrame", true) +_TimerService:SetTimerOnce(function() + if m.poison ~= nil and m.poison > 0 then + m.hp = m.hp - m.poison + self:ShowDmgPop(idx, m.poison) + self:MonsterHitMotion(idx) + m.poison = m.poison - 1 + if m.hp <= 0 then + m.hp = 0 + self:KillMonster(m.slot) + self:RenderCombat() + self:SetEntityEnabled(base .. "/ActFrame", false) + _TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15) + return + end + end + m.block = 0 + local intent = m.intents[m.intentIdx] + if intent ~= nil then + if intent.kind == "Attack" then + self:MonsterLunge(idx) + 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 + local before = self.PlayerHp + self:DealDamageToPlayer(atk, idx) + self:ShowPlayerDmgPop(before - self.PlayerHp) + self:PlayerHitMotion() + elseif intent.kind == "Defend" then + m.block = m.block + intent.value + elseif intent.kind == "Debuff" then + if intent.effect == "weak" then + self.PlayerWeak = self.PlayerWeak + intent.value + elseif intent.effect == "vuln" then + self.PlayerVuln = self.PlayerVuln + intent.value + end + elseif intent.kind == "AddCard" then + local cnt = intent.count or 1 + for ci = 1, cnt do + table.insert(self.DiscardPile, intent.card) + end + self:RenderPiles() + local cn = intent.card + local cc = self.Cards[intent.card] + if cc ~= nil then cn = cc.name end + self:Toast(m.name .. ": " .. cn .. " 추가!") + end + end + if #m.intents > 0 then + m.intentIdx = math.random(1, #m.intents) + end + if m.weak > 0 then m.weak = m.weak - 1 end + if m.vuln > 0 then m.vuln = m.vuln - 1 end + self:RenderCombat() + self:SetEntityEnabled(base .. "/ActFrame", false) + _TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15) +end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]), + method('FinishEnemyTurn', `self.TurnBusy = false +self:CheckCombatEnd() +if self.CombatOver == true then + return +end +_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`), + method('ClearCombatCards', `self.DrawPile = {} +self.DiscardPile = {} +self.ExhaustPile = {} +self.Hand = {} +self.DiscardSelectRemaining = 0 +self.DiscardSelectTotal = 0 +self.DiscardPostShiv = 0 +self.DiscardShivPerPick = 0 +self:UpdateDiscardPrompt() +self:RenderHand(false) +self:RenderPiles()`), + method('CheckCombatEnd', `local anyAlive = false +for i = 1, #self.Monsters do + if self.Monsters[i].alive == true then anyAlive = true; break end +end +if anyAlive == false then + self.CombatOver = true + self:ClearCombatCards() + self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult()) + self:ApplyRelics("combatEnd") + self:ApplyRelics("combatReward") + self:MaybeDropPotion() + self:RenderRun() + local node = self.MapNodes[self.CurrentNodeId] + if node ~= nil and node.type == "elite" then + self.Gold = self.Gold + 15 + local nid = self:PickNewRelic() + if nid ~= "" then + self:AddRelic(nid) + local nr = self.Relics[nid] + if nr ~= nil then + self:Toast("유물 획득: " .. nr.name) + end + end + end + if node ~= nil and node.type == "boss" then + if self.PlayerJob == "" and self.Floor < self.RunLength then + self:ShowJobChoice() + else + if self.PlayerJob ~= "" then self:AwardSouls(1) end + local bid = self:PickNewRelic() + if bid ~= "" then + self:AddRelic(bid) + local br = self.Relics[bid] + if br ~= nil then + self:Toast("유물 획득: " .. br.name) + end + end + self:ContinueAfterBoss() + end + else + self:OfferReward() + end +elseif self.PlayerHp <= 0 then + self.CombatOver = true + self:EndRun("패배...") +end`), + method('ContinueAfterBoss', `if self.Floor < self.RunLength then + self.Floor = self.Floor + 1 + self.CurrentNodeId = "" + self.CurrentEnemyId = "" + self:GenerateMap() + self:RenderRun() + self:TeleportToActMap() + self:ShowMap() +else + self:EndRun("런 클리어!") +end`), +]; diff --git a/tools/deck/cb/deckturn.mjs b/tools/deck/cb/deckturn.mjs new file mode 100644 index 0000000..2323968 --- /dev/null +++ b/tools/deck/cb/deckturn.mjs @@ -0,0 +1,344 @@ +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 deckTurnMethods = [ + method('Shuffle', `if list == nil then +\treturn +end +for i = #list, 2, -1 do +\tlocal j = math.random(1, i) +\tlist[i], list[j] = list[j], list[i] +end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]), + method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton") +if endTurn ~= nil and endTurn.ButtonComponent ~= nil then + if self.EndTurnHandler ~= nil then + endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler) + self.EndTurnHandler = nil + end + self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end) +end +local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile") +if drawPile ~= nil and drawPile.ButtonComponent ~= nil then + if self.DrawPileHandler ~= nil then + drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler) + self.DrawPileHandler = nil + end + self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end) +end +local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile") +if discardPile ~= nil and discardPile.ButtonComponent ~= nil then + if self.DiscardPileHandler ~= nil then + discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler) + self.DiscardPileHandler = nil + end + self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end) +end +local exhaustPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/ExhaustPile") +if exhaustPile ~= nil and exhaustPile.ButtonComponent ~= nil then + if self.ExhaustPileHandler ~= nil then + exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler) + self.ExhaustPileHandler = nil + end + self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end) +end +local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close") +if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then + if self.DeckInspectCloseHandler ~= nil then + inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler) + self.DeckInspectCloseHandler = nil + end + self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end) +end +local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton") +if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then + if self.AllDeckHandler ~= nil then + allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler) + self.AllDeckHandler = nil + end + self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end) +end +local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") +if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then + if self.AllDeckCloseHandler ~= nil then + allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) + self.AllDeckCloseHandler = nil + end + self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) +end +self:BindClassDeckTabs() +for i = 1, 10 do + local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) + if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then + local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i) + cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) + cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) + cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end) + cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end) + cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end) + cardEntity:ConnectEvent(UITouchEnterEvent, function() self:HoverCard(i) end) + cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end) + if cardEntity.ButtonComponent ~= nil then + cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end) + end + end +end +for i = 1, 3 do + local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)) + if rc ~= nil and rc.ButtonComponent ~= nil then + rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end) + if rc.UITouchReceiveComponent ~= nil then + local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i) + rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) + rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) + end + end +end +local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip") +if skip ~= nil and skip.ButtonComponent ~= nil then + skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end) +end +local mapNodeIds = {} +for r = 1, ${MAP_ROWS} do + for c = 1, ${MAP_COLS} do + table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c)) + end +end +table.insert(mapNodeIds, "boss") +for i = 1, #mapNodeIds do + local nid = mapNodeIds[i] + local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid) + if mn ~= nil and mn.ButtonComponent ~= nil then + mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end) + end +end +for i = 1, 3 do + local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i)) + if sc ~= nil and sc.ButtonComponent ~= nil then + sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end) + if sc.UITouchReceiveComponent ~= nil then + local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i) + sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) + sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) + end + end +end +local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave") +if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then + shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) +end +local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic") +if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then + shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end) +end +local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave") +if restLeave ~= nil and restLeave.ButtonComponent ~= nil then + restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) +end +for i = 1, ${MAX_MONSTERS} do + local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)) + if ms ~= nil and ms.ButtonComponent ~= nil then + ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end) + end +end +for i = 1, 10 do + local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)) + if rs ~= nil and rs.UITouchReceiveComponent ~= nil then + local idx = i + rs:ConnectEvent(UITouchEnterEvent, function() + local rid = nil + if self.RunRelics ~= nil then rid = self.RunRelics[idx] end + if rid ~= nil and self.Relics[rid] ~= nil then + self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48) + end + end) + rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end) + end +end +for i = 1, 5 do + local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)) + if ps ~= nil and ps.UITouchReceiveComponent ~= nil then + local idx = i + ps:ConnectEvent(UITouchEnterEvent, function() + local pid = nil + if self.RunPotions ~= nil then pid = self.RunPotions[idx] end + if pid ~= nil and self.Potions[pid] ~= nil then + self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44) + end + end) + ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end) + ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end) + end +end +local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use") +if pmUse ~= nil and pmUse.ButtonComponent ~= nil then + pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end) +end +local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss") +if pmToss ~= nil and pmToss.ButtonComponent ~= nil then + pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end) +end +local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close") +if pmClose ~= nil and pmClose.ButtonComponent ~= nil then + pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end) +end +local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion") +if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then + shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end) +end +local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") +if chest ~= nil and chest.ButtonComponent ~= nil then + chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end) +end +local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave") +if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then + treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) +end +local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton") +if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then + jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end) +end +local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton") +if jcJob ~= nil and jcJob.ButtonComponent ~= nil then + jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end) +end +for i = 1, 3 do + local slotIdx = i + local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)) + if jb ~= nil and jb.ButtonComponent ~= nil then + jb:ConnectEvent(ButtonClickEvent, function() + if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then + self:SetJob(self.JobOpts[slotIdx].id) + end + end) + end +end`), + method('StartPlayerTurn', `self.Turn = self.Turn + 1 +self.Energy = self.MaxEnergy +self:ApplyRelics("turnStart") +self.PlayerBlock = 0 +if self.ClayBlockNext > 0 then + self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext + self.ClayBlockNext = 0 +end +if self.PlayerPowers ~= nil then + for i = 1, #self.PlayerPowers do + local pc = self.Cards[self.PlayerPowers[i]] + if pc ~= nil then + if pc.powerEffect == "strengthPerTurn" then + self.PlayerStr = self.PlayerStr + pc.value + elseif pc.powerEffect == "energyPerTurn" then + self.Energy = self.Energy + pc.value + elseif pc.powerEffect == "blockPerTurn" then + self.PlayerBlock = self.PlayerBlock + pc.value + end + if pc.turnStartShiv ~= nil then + self:AddCardsToHand("Shiv", pc.turnStartShiv) + end + end + end +end +self:DrawCards(5) +self:RenderHand(true) +self:RenderCombat()`), + method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then + return +end +if self:IsDiscardSelecting() == true then + self:Toast("버릴 카드를 먼저 선택하세요") + return +end +local burn = 0 +for bi = 1, #self.Hand do +\tlocal hc = self.Cards[self.Hand[bi]] +\tif hc ~= nil and hc.endTurnDamage ~= nil then burn = burn + hc.endTurnDamage end +end +if burn > 0 then +\tself.PlayerHp = self.PlayerHp - burn +\tif self.PlayerHp < 0 then self.PlayerHp = 0 end +\tself:ShowPlayerDmgPop(burn) +\tself:RenderCombat() +end +local kept = {} +for i = 1, #self.Hand do +\tlocal cardId = self.Hand[i] +\tlocal c = self.Cards[cardId] +\tif c ~= nil and c.retain == true then +\t\ttable.insert(kept, cardId) +\telse +\t\ttable.insert(self.DiscardPile, cardId) +\tend +end +self.Hand = kept +if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end +if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end +self:RenderHand(false) +self:RenderPiles() +self:EnemyTurn()`), + method('DrawCards', `local drawnSlots = {} +for i = 1, amount do +\tif #self.DrawPile <= 0 then +\t\tself:RecycleDiscardIntoDraw() +\tend +\tif #self.DrawPile <= 0 then +\t\tbreak +\tend +\tlocal cardId = table.remove(self.DrawPile) +\tif #self.Hand >= 10 then +\t\ttable.insert(self.DiscardPile, cardId) +\t\tself:TriggerSly(cardId) +\telse +\t\ttable.insert(self.Hand, cardId) +\t\tif #self.Hand <= 5 then +\t\t\ttable.insert(drawnSlots, #self.Hand) +\t\tend +\tend +end +self:RenderPiles() +if animate == true and #drawnSlots > 0 then +\tself:RenderHand(false) +\tlocal drawStart = Vector2(-590, 8) +\tfor i = 1, #drawnSlots do +\t\tlocal slot = drawnSlots[i] +\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045) +\tend +end`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }, + ]), + method('AddCardsToHand', `if self.Hand == nil then + self.Hand = {} +end +if self.DiscardPile == nil then + self.DiscardPile = {} +end +for i = 1, amount do + if #self.Hand >= 10 then + table.insert(self.DiscardPile, cardId) + else + table.insert(self.Hand, cardId) + end +end +self:RenderHand(false) +self:RenderPiles()`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, + ]), + method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then +\treturn +end +self.DrawPile = {} +for i = 1, #self.DiscardPile do +\tself.DrawPile[i] = self.DiscardPile[i] +end +self.DiscardPile = {} +self:Shuffle(self.DrawPile)`), + method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile)) +self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile)) +self:SetText("/ui/DefaultGroup/DeckHud/ExhaustPile/Count", self:FormatNumber(#(self.ExhaustPile or {}))) +self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy)) +local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") +if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then + self:OpenDeckInspect(self.DeckInspectKind) +end`), +]; diff --git a/tools/deck/cb/deckview.mjs b/tools/deck/cb/deckview.mjs new file mode 100644 index 0000000..d47b463 --- /dev/null +++ b/tools/deck/cb/deckview.mjs @@ -0,0 +1,229 @@ +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 deckViewMethods = [ + method('OpenDeckInspect', `self.DeckInspectKind = kind +if self.DeckAllOpen == true then + self.DeckAllOpen = false + local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") + if allHud ~= nil then + allHud.Enable = false + end +end +local pile = {} +local title = "" +if kind == "discard" then + pile = self.DiscardPile or {} + title = "버린 덱" +elseif kind == "exhaust" then + pile = self.ExhaustPile or {} + title = "소멸 덱" +else + pile = self.DrawPile or {} + title = "뽑을 덱" +end +self:RenderDeckInspect(pile, title) +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") +if hud ~= nil then + hud.Enable = true +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), + method('CloseDeckInspect', `self.DeckInspectKind = "" +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") +if hud ~= nil then + hud.Enable = false +end`), + method('RenderDeckInspect', `local count = 0 +if pile ~= nil then + count = #pile +end +local suffix = " (" .. tostring(count) .. ")" +if count > 60 then + suffix = suffix .. " - 60장까지 표시" +end +self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix) +local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty") +if empty ~= nil then + empty.Enable = count <= 0 +end +for i = 1, 60 do + local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i)) + if e ~= nil then + local cardId = nil + if pile ~= nil then + cardId = pile[i] + end + if cardId == nil then + e.Enable = false + else + e.Enable = true + self:ApplyInspectCardVisual(i, cardId) + end + end +end`, [ + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' }, + ]), + method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + ]), + method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/WarriorTab") +if warriorTab ~= nil and warriorTab.ButtonComponent ~= nil then + if self.WarriorDeckTabHandler ~= nil then + warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler) + self.WarriorDeckTabHandler = nil + end + self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end) +end +local thiefTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/ThiefTab") +if thiefTab ~= nil and thiefTab.ButtonComponent ~= nil then + if self.ThiefDeckTabHandler ~= nil then + thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler) + self.ThiefDeckTabHandler = nil + end + self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end) +end +local mageTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/MageTab") +if mageTab ~= nil and mageTab.ButtonComponent ~= nil then + if self.MageDeckTabHandler ~= nil then + mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler) + self.MageDeckTabHandler = nil + end + self.MageDeckTabHandler = mageTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("magician") end) +end`), + method('OpenClassDeck', `self.CodexMode = false +self.ClassDeckMode = true +self.DeckAllOpen = true +self:SetClassDeckTab(className) +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") +if hud ~= nil then + hud.Enable = true +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]), + method('SetClassDeckTab', `if self.ClassDeckMode ~= true then + return +end +self.ClassDeckCards = {} +self.ClassDeckTitle = "직업 덱" +if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then + className = "bandit" +end +self.ClassDeckClass = className +local allowed = {} +if className == "warrior" then + allowed["warrior"] = true + allowed["fighter"] = true + allowed["page"] = true + allowed["spearman"] = true + self.ClassDeckTitle = "전사 전체 덱" +elseif className == "magician" then + allowed["magician"] = true + allowed["firepoison"] = true + allowed["icelightning"] = true + allowed["cleric"] = true + self.ClassDeckTitle = "마법사 전체 덱" +else + allowed["bandit"] = true + allowed["shiv"] = true + allowed["poisoner"] = true + allowed["trickster"] = true + self.ClassDeckTitle = "도적 전체 덱" +end +for id, c in pairs(self.Cards) do + if c ~= nil and c.curse ~= true and allowed[c.class] == true then + table.insert(self.ClassDeckCards, id) + end +end +table.sort(self.ClassDeckCards, function(a, b) + local ca = self.Cards[a] + local cb = self.Cards[b] + local na = a + local nb = b + if ca ~= nil and ca.name ~= nil then na = ca.name end + if cb ~= nil and cb.name ~= nil then nb = cb.name end + if na == nb then return a < b end + return na < nb +end) +self:RenderAllDeck() +self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]), + method('RenderClassDeckTabs', `local tabs = { + { path = "/ui/DefaultGroup/DeckAllHud/WarriorTab", cls = "warrior" }, + { path = "/ui/DefaultGroup/DeckAllHud/ThiefTab", cls = "bandit" }, + { path = "/ui/DefaultGroup/DeckAllHud/MageTab", cls = "magician" }, +} +for i = 1, #tabs do + local e = _EntityService:GetEntityByPath(tabs[i].path) + if e ~= nil then + e.Enable = self.ClassDeckMode == true + if e.SpriteGUIRendererComponent ~= nil then + if self.ClassDeckClass == tabs[i].cls then + e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1) + else + e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1) + end + end + end +end`), + method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") +if inspectHud ~= nil then + inspectHud.Enable = false +end +self.DeckInspectKind = "" +self.ClassDeckMode = false +self.ClassDeckClass = "" +self:RenderClassDeckTabs() +self.DeckAllOpen = true +self:RenderAllDeck() +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") +if hud ~= nil then + hud.Enable = true +end`), + method('CloseAllDeck', `self.DeckAllOpen = false +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") +if hud ~= nil then + hud.Enable = false +end +if self.ClassDeckMode == true then + self.ClassDeckMode = false + self.ClassDeckCards = {} + self.ClassDeckTitle = "" + self.ClassDeckClass = "" +end +self:RenderClassDeckTabs() +if self.CodexMode == true then + self.CodexMode = false + self:ShowLobby() +end`), + method('RenderAllDeck', `local pile = self.RunDeck or {} +local title = "모든 덱" +if self.ClassDeckMode == true then + pile = self.ClassDeckCards or {} + title = self.ClassDeckTitle +elseif self.CodexMode == true then + pile = self.CodexCards or {} + title = "카드 도감" +end +local count = #pile +self:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")") +self:RenderClassDeckTabs() +local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty") +if empty ~= nil then + empty.Enable = count <= 0 +end +for i = 1, 120 do + local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i)) + if e ~= nil then + local cardId = pile[i] + if cardId == nil then + e.Enable = false + else + e.Enable = true + self:ApplyAllDeckCardVisual(i, cardId) + end + end +end`), + method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + ]), +]; diff --git a/tools/deck/cb/hand.mjs b/tools/deck/cb/hand.mjs new file mode 100644 index 0000000..7a1af3c --- /dev/null +++ b/tools/deck/cb/hand.mjs @@ -0,0 +1,401 @@ +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 handMethods = [ + method('GetHandSlotX', `local n = 0 +if self.Hand ~= nil then + n = #self.Hand +end +if n <= 0 then + return 0 +end +local spacing = 175 +if n > 8 then spacing = math.floor(1400 / n) end +local startX = -((n - 1) * spacing) / 2 +return startX + (slot - 1) * spacing`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'), + method('RenderHand', `local n = #self.Hand +local spacing = 175 +if n > 8 then spacing = math.floor(1400 / n) end +local startX = -((n - 1) * spacing) / 2 +local drawStart = Vector2(-590, 8) +for i = 1, 10 do +\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) +\tif cardEntity ~= nil then +\t\tlocal cardId = self.Hand[i] +\t\tif cardId == nil then +\t\t\tcardEntity.Enable = false +\t\telse +\t\t\tcardEntity.Enable = true +\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end +\t\t\tself:ApplyCardVisual(i, cardId) +\t\t\tlocal tx = self:GetHandSlotX(i) +\t\t\tif animate == true then +\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03) +\t\t\telse +\t\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.anchoredPosition = Vector2(tx, 0) end +\t\t\tend +\t\tend +\tend +end +self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]), + method('ApplyCardFace', `local c = self.Cards[cardId] +if c == nil then + c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" } +end +local e = _EntityService:GetEntityByPath(base) +if e ~= nil and e.SpriteGUIRendererComponent ~= nil then + if e.UITransformComponent ~= nil then + e.UITransformComponent.UIScale = Vector3(1, 1, 1) + end + local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"] + local ruid = nil + if frames ~= nil then + ruid = frames[c.rarity or "normal"] + end + if ruid ~= nil then + e.SpriteGUIRendererComponent.ImageRUID = ruid + e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) + end +end +self:SetText(base .. "/Cost", string.format("%d", c.cost)) +self:SetText(base .. "/Name", c.name) +self:SetText(base .. "/Desc", c.desc) +local art = _EntityService:GetEntityByPath(base .. "/Art") +if art ~= nil then + if c.image ~= nil and c.image ~= "" then + art.Enable = true + if art.SpriteGUIRendererComponent ~= nil then + art.SpriteGUIRendererComponent.ImageRUID = c.image + end + else + art.Enable = false + end +end`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + ]), + method('SetCardHover', `local prefix = "" +local count = 0 +local xs = {} +local baseY = 0 +local hoverIndex = 0 +local push = 110 +if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then + if self.DragSlot ~= nil and self.DragSlot > 0 then + return + end + prefix = "/ui/DefaultGroup/CardHand/Card" + count = 0 + if self.Hand ~= nil then count = #self.Hand end + for i = 1, count do + xs[i] = self:GetHandSlotX(i) + end + baseY = 0 + hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 +elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then + prefix = "/ui/DefaultGroup/RewardHud/Reward" + count = 3 + xs = { -300, 0, 300 } + baseY = 0 + hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0 +elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then + prefix = "/ui/DefaultGroup/ShopHud/Card" + count = 3 + xs = { -300, 0, 300 } + baseY = 20 + hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 +end +if count <= 0 then + return +end +if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then + _TimerService:ClearTimer(self.CardHoverTweenId) + self.CardHoverTweenId = 0 +end +local items = {} +for i = 1, count do + local e = _EntityService:GetEntityByPath(prefix .. tostring(i)) + if e ~= nil and e.UITransformComponent ~= nil then + local tr = e.UITransformComponent + local tx = xs[i] + local ty = baseY + local sc = 1 + if hover == true and hoverIndex > 0 then + if i == hoverIndex and e.Enable == true then + sc = 1.5 + elseif i < hoverIndex then + tx = tx - push + elseif i > hoverIndex then + tx = tx + push + end + end + table.insert(items, { tr = tr, sx = tr.anchoredPosition.x, sy = tr.anchoredPosition.y, ss = tr.UIScale.x, tx = tx, ty = ty, ts = sc }) + end +end +local elapsed = 0 +local duration = 0.12 +local eventId = 0 +eventId = _TimerService:SetTimerRepeat(function() + elapsed = elapsed + 1 / 60 + local t = math.min(elapsed / duration, 1) + local eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) + for i = 1, #items do + local it = items[i] + local x = it.sx + (it.tx - it.sx) * eased + local y = it.sy + (it.ty - it.sy) * eased + local s = it.ss + (it.ts - it.ss) * eased + it.tr.anchoredPosition = Vector2(x, y) + it.tr.UIScale = Vector3(s, s, 1) + end + if t >= 1 then + _TimerService:ClearTimer(eventId) + if self.CardHoverTweenId == eventId then + self.CardHoverTweenId = 0 + end + end +end, 1 / 60) +self.CardHoverTweenId = eventId`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' }, + ]), + method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + ]), + method('SetText', `local entity = _EntityService:GetEntityByPath(path) +if entity ~= nil and entity.TextComponent ~= nil then +\tentity.TextComponent.Text = value +end`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }, + ]), + method('FormatNumber', `if value == nil then + return "" +end +local n = tonumber(value) +if n == nil then + return tostring(value) +end +if math.abs(n - math.floor(n)) < 0.00001 then + return string.format("%d", math.floor(n)) +end +return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'), + method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) +if cardEntity == nil or cardEntity.UITransformComponent == nil then +\treturn +end +local tr = cardEntity.UITransformComponent +tr.anchoredPosition = fromPos +local elapsed = 0 +local eventId = 0 +eventId = _TimerService:SetTimerRepeat(function() +\telapsed = elapsed + 1 / 60 +\tlocal t = math.min(elapsed / duration, 1) +\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) +\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased) +\tif t >= 1 then +\t\t_TimerService:ClearTimer(eventId) +\tend +end, 1 / 60)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' }, + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' }, + ]), + method('AddCardBlock', `local amount = base or 0 +if amount > 0 and self.PlayerDex ~= nil then + amount = amount + self.PlayerDex +end +if amount < 0 then + amount = 0 +end +self.PlayerBlock = self.PlayerBlock + amount +return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), + method('CalcPlayerAttack', `local base2 = base +self.FightAttackCount = self.FightAttackCount + 1 +if self.FightAttackCount == 1 and self:HasRelic("akabeko") then + base2 = base2 + 8 +end +local dmg = base2 + self.PlayerStr +if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then + dmg = dmg * 2 +end +if self.PlayerWeak > 0 then + dmg = math.floor(dmg * 0.75) +end +if dmg > 0 and dmg < 5 and self:HasRelic("boot") then + dmg = 5 +end +if dmg < 0 then + dmg = 0 +end +return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), + method('ResolveCardEffects', `if c == nil then + return +end +if c.kind == "Attack" then + if c.damage ~= nil then + self:PlayerAttackMotion() + local total = 0 + local hitN = c.hits or 1 + for h = 1, hitN do + total = total + self:CalcPlayerAttack(c.damage) + end + if c.aoe == true then + self:PlayAoeFx(c.fx or c.image, total) + else + self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true) + end + end + if c.block ~= nil then + self:AddCardBlock(c.block) + end + if free ~= true then + self:ApplyRelics("cardPlayed") + end +elseif c.kind == "Skill" then + if c.block ~= nil then + self:AddCardBlock(c.block) + end +elseif c.kind == "Power" then + if free ~= true then + table.insert(self.PlayerPowers, cardId) + end +end +if c.strength ~= nil then + self.PlayerStr = self.PlayerStr + c.strength +end +if c.dex ~= nil then + self.PlayerDex = self.PlayerDex + c.dex +end +if c.thorns ~= nil then + self.PlayerThorns = self.PlayerThorns + c.thorns +end +if c.selfVuln ~= nil then + self.PlayerVuln = self.PlayerVuln + c.selfVuln +end +if c.heal ~= nil then + self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp) +end +if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then + local tm = self.Monsters[self.TargetIndex] + if tm == nil or tm.alive ~= true then + for i = 1, #self.Monsters do + if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end + end + end + if tm ~= nil and tm.alive == true then + if c.weak ~= nil then tm.weak = tm.weak + c.weak end + if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end + if c.vuln ~= nil then + tm.vuln = tm.vuln + c.vuln + if self:HasRelic("championBelt") then + tm.weak = tm.weak + 1 + end + end + end +end +if c.draw ~= nil then + self:DrawCards(c.draw, true) +end +if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then + self:AddCardsToHand("Shiv", c.addShiv) +end`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' }, + ]), + method('TriggerSly', `local c = self.Cards[cardId] +if c == nil or c.sly ~= true then + return +end +self:Toast("교활 발동: " .. c.name) +self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]), + method('DiscardHandCard', `if self.Hand == nil then + return +end +local cardId = self.Hand[slot] +if cardId == nil then + return +end +table.remove(self.Hand, slot) +table.insert(self.DiscardPile, cardId) +if triggerSly == true then + self:TriggerSly(cardId) +end`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' }, + ]), + method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'), + method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt") +if e == nil then + return +end +if self:IsDiscardSelecting() == true then + local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining + self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. self:FormatNumber(picked + 1) .. "/" .. self:FormatNumber(self.DiscardSelectTotal)) + e.Enable = true +else + e.Enable = false +end`), + method('BeginDiscardSelection', `if c == nil or self.Hand == nil then + return false +end +local n = 0 +if c.discardAll == true then + n = #self.Hand +elseif c.discard ~= nil then + n = math.min(c.discard, #self.Hand) +end +if n <= 0 then + return false +end +self.DiscardSelectRemaining = n +self.DiscardSelectTotal = n +self.DiscardPostShiv = 0 +self.DiscardShivPerPick = 0 +if c.addShiv ~= nil then + self.DiscardPostShiv = c.addShiv +end +if c.addShivPerDiscard == true then + self.DiscardShivPerPick = 1 +end +self:UpdateDiscardPrompt() +self:Toast("버릴 카드를 선택하세요") +return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'), + method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0 +self.DiscardSelectTotal = 0 +local shivCount = self.DiscardPostShiv or 0 +self.DiscardPostShiv = 0 +self.DiscardShivPerPick = 0 +self:UpdateDiscardPrompt() +if shivCount > 0 then + self:AddCardsToHand("Shiv", shivCount) +end +self:RenderHand(false) +self:RenderPiles() +self:RenderCombat() +self:CheckCombatEnd()`), + method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then + return false +end +if self.Hand == nil or self.Hand[slot] == nil then + return true +end +local discarded = self.Hand[slot] +self:DiscardHandCard(slot, true) +if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then + self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick +end +self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1 +if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then + self:FinishDiscardSelection() +else + self:UpdateDiscardPrompt() + self:RenderHand(false) + self:RenderPiles() + self:RenderCombat() +end +return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'), +]; diff --git a/tools/deck/cb/items.mjs b/tools/deck/cb/items.mjs new file mode 100644 index 0000000..4915adf --- /dev/null +++ b/tools/deck/cb/items.mjs @@ -0,0 +1,213 @@ +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 itemMethods = [ + method('HasRelic', `if self.RunRelics == nil then + return false +end +for i = 1, #self.RunRelics do + if self.RunRelics[i] == id then + return true + end +end +return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'), + method('ApplyRelics', `if self.RunRelics == nil then + return +end +for i = 1, #self.RunRelics do + local r = self.Relics[self.RunRelics[i]] + if r ~= nil and r.hook == hook then + if r.effect == "block" then + self.PlayerBlock = self.PlayerBlock + r.value + elseif r.effect == "energy" then + self.Energy = self.Energy + r.value + elseif r.effect == "strength" then + self.PlayerStr = self.PlayerStr + r.value + elseif r.effect == "draw" then + self:DrawCards(r.value) + self:RenderHand(false) + elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then + self.PlayerHp = self.PlayerHp + r.value + if self.PlayerHp > self.PlayerMaxHp then + self.PlayerHp = self.PlayerMaxHp + end + elseif r.effect == "healIfLow" then + if self.PlayerHp * 2 <= self.PlayerMaxHp then + self.PlayerHp = self.PlayerHp + r.value + if self.PlayerHp > self.PlayerMaxHp then + self.PlayerHp = self.PlayerMaxHp + end + end + elseif r.effect == "gold" then + self.Gold = self.Gold + r.value + end + end +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]), + method('AddRelic', `if self.RunRelics == nil then + self.RunRelics = {} +end +table.insert(self.RunRelics, id) +local r = self.Relics[id] +if r ~= nil and r.hook == "passive" then + if r.effect == "potionSlots" then + self.PotionSlots = r.value + self:RenderPotions() + elseif r.effect == "maxHp" then + self.PlayerMaxHp = self.PlayerMaxHp + r.value + self.PlayerHp = self.PlayerHp + r.value + end +end +self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), + method('PickNewRelic', `local pool = {} +for i = 1, #self.RelicPool do + if self:HasRelic(self.RelicPool[i]) == false then + table.insert(pool, self.RelicPool[i]) + end +end +if #pool == 0 then + self.Gold = self.Gold + 25 + self:Toast("유물을 모두 모았습니다! 메소 +25") + return "" +end +return pool[math.random(1, #pool)]`, [], 0, 'string'), + method('AddPotion', `if self.RunPotions == nil then + self.RunPotions = {} +end +if #self.RunPotions >= self.PotionSlots then + self:Toast("물약 슬롯이 가득 찼습니다") + return false +end +table.insert(self.RunPotions, pid) +self:RenderPotions() +return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'), + method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then + return +end +local keys = {} +for pid, _ in pairs(self.Potions) do + table.insert(keys, pid) +end +table.sort(keys) +local pid = keys[math.random(1, #keys)] +if self:AddPotion(pid) == true then + local p = self.Potions[pid] + self:Toast("물약 획득: " .. p.name) +end`), + method('RenderPotions', `for i = 1, 5 do + local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i) + local e = _EntityService:GetEntityByPath(base) + if e ~= nil and e.SpriteGUIRendererComponent ~= nil then + local pid = nil + if self.RunPotions ~= nil then + pid = self.RunPotions[i] + end + if pid ~= nil and self.Potions[pid] ~= nil then + e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon + e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) + elseif i > self.PotionSlots then + e.SpriteGUIRendererComponent.ImageRUID = "" + e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85) + else + e.SpriteGUIRendererComponent.ImageRUID = "" + e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9) + end + end +end`), + method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then + return +end +self.PotionMenuSlot = slot +local pid = self.RunPotions[slot] +local p = self.Potions[pid] +if p ~= nil then + self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc) +end +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('ClosePotionMenu', `self.PotionMenuSlot = 0 +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`), + method('UsePotion', `if self.PotionMenuSlot <= 0 then + return +end +if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then + self:Toast("지금은 사용할 수 없습니다") + return +end +local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud") +local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand") +if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then + self:Toast("전투 중에만 사용할 수 있습니다") + return +end +local pid = self.RunPotions[self.PotionMenuSlot] +if pid == nil then + return +end +local p = self.Potions[pid] +if p == nil then + return +end +if p.effect == "heal" then + self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp) +elseif p.effect == "damage" then + self:DealDamageToTarget(p.value, false) + self:ShowDmgPop(self.TargetIndex, p.value) +elseif p.effect == "strength" then + self.PlayerStr = self.PlayerStr + p.value +elseif p.effect == "block" then + self.PlayerBlock = self.PlayerBlock + p.value +elseif p.effect == "energy" then + self.Energy = self.Energy + p.value +elseif p.effect == "weak" then + local tm = self.Monsters[self.TargetIndex] + if tm ~= nil and tm.alive == true then + tm.weak = tm.weak + p.value + end +end +table.remove(self.RunPotions, self.PotionMenuSlot) +self:Toast("물약 사용: " .. p.name) +self:ClosePotionMenu() +self:RenderPotions() +self:RenderPiles() +self:RenderCombat() +self:CheckCombatEnd()`), + method('TossPotion', `if self.PotionMenuSlot <= 0 then + return +end +local pid = self.RunPotions[self.PotionMenuSlot] +if pid ~= nil then + local p = self.Potions[pid] + table.remove(self.RunPotions, self.PotionMenuSlot) + if p ~= nil then + self:Toast("물약 버림: " .. p.name) + end +end +self:ClosePotionMenu() +self:RenderPotions()`), + method('RenderRelics', `local count = 0 +if self.RunRelics ~= nil then + count = #self.RunRelics +end +for i = 1, 10 do + local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i) + local e = _EntityService:GetEntityByPath(base) + if e ~= nil and e.SpriteGUIRendererComponent ~= nil then + local rid = nil + if self.RunRelics ~= nil then + rid = self.RunRelics[i] + end + if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then + e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon + e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) + else + e.SpriteGUIRendererComponent.ImageRUID = "" + e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6) + end + end +end +local of = "" +if count > 10 then + of = "+" .. tostring(count - 9) +end +self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`), +]; diff --git a/tools/deck/cb/jobs.mjs b/tools/deck/cb/jobs.mjs new file mode 100644 index 0000000..6030c5a --- /dev/null +++ b/tools/deck/cb/jobs.mjs @@ -0,0 +1,79 @@ +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 jobMethods = [ + method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) +self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`), + method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) +if kind == "relic" then + local bid = self:PickNewRelic() + if bid ~= "" then + self:AddRelic(bid) + local br = self.Relics[bid] + if br ~= nil then + self:Toast("유물 획득: " .. br.name) + end + end + self:ContinueAfterBoss() +else + self:ShowJobSelect() +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), + method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass] +if opts == nil then + opts = self.Jobs["warrior"] +end +self.JobOpts = opts +for i = 1, 3 do + local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i) + local o = opts[i] + if o ~= nil then + self:SetEntityEnabled(base, true) + self:SetText(base .. "/Name", o.name) + self:SetText(base .. "/Desc", o.desc) + local sc = self.Cards[o.starter] + if sc ~= nil then + self:SetText(base .. "/Starter", "대표 카드: " .. sc.name) + end + else + self:SetEntityEnabled(base, false) + end +end +self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`), + method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then + for cls, list in pairs(self.Jobs) do + for i = 1, #list do + if list[i].id == self.PlayerJob then + return list[i].name + end + end + end +end +if self.SelectedClass == "warrior" then + return "전사" +elseif self.SelectedClass == "bandit" then + return "도적" +elseif self.SelectedClass == "magician" then + return "마법사" +end +return "플레이어"`, [], 0, 'string'), + method('SetJob', `self.PlayerJob = jobId +local starter = "" +local opts = self.Jobs[self.SelectedClass] or {} +for i = 1, #opts do + if opts[i].id == jobId then + starter = opts[i].starter + end +end +if starter ~= "" then + table.insert(self.RunDeck, starter) + local sc = self.Cards[starter] + if sc ~= nil then + self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name) + end +end +self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) +self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) +self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]), +]; diff --git a/tools/deck/cb/map.mjs b/tools/deck/cb/map.mjs new file mode 100644 index 0000000..57a7d95 --- /dev/null +++ b/tools/deck/cb/map.mjs @@ -0,0 +1,230 @@ +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 mapMethods = [ + method('ShowMap', `self:ShowState("map") +self:RenderMap()`), + method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지 +self.MapNodes = {} +self.MapStart = {} +self.VisitedNodes = {} +self.Depth = 0 +self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} } +local cols = { 1, 2, 3, 4 } +for i = #cols, 2, -1 do + local j = math.random(1, i) + cols[i], cols[j] = cols[j], cols[i] +end +local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) } +for p = 1, 4 do + local c = starts[p] + local sid = "r1c" .. tostring(c) + if self.MapNodes[sid] == nil then + self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} } + end + local found = false + for i = 1, #self.MapStart do + if self.MapStart[i] == sid then found = true end + end + if found == false then + table.insert(self.MapStart, sid) + end + for r = 1, ${MAP_ROWS} - 1 do + local nc = c + math.random(-1, 1) + if nc < 1 then nc = 1 end + if nc > ${MAP_COLS} then nc = ${MAP_COLS} end + local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc) + if self.MapNodes[nid] == nil then + self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} } + end + local fid = "r" .. tostring(r) .. "c" .. tostring(c) + local dup = false + for i = 1, #self.MapNodes[fid].next do + if self.MapNodes[fid].next[i] == nid then dup = true end + end + if dup == false then + table.insert(self.MapNodes[fid].next, nid) + end + c = nc + end + local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) + local bdup = false + for i = 1, #self.MapNodes[lid].next do + if self.MapNodes[lid].next[i] == "boss" then bdup = true end + end + if bdup == false then + table.insert(self.MapNodes[lid].next, "boss") + end +end +for r = 3, ${MAP_ROWS} do + for c = 1, ${MAP_COLS} do + local id = "r" .. tostring(r) .. "c" .. tostring(c) + local node = self.MapNodes[id] + if node ~= nil then + -- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지) + local parentTypes = {} + for pid, pn in pairs(self.MapNodes) do + if pn.row == r - 1 then + for i = 1, #pn.next do + if pn.next[i] == id then parentTypes[pn.type] = true end + end + end + end + local w + if r == ${MAP_ROWS} then + w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } } + elseif r >= 4 then + w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } } + else + w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } } + end + local total = 0 + for i = 1, #w do + local t = w[i][1] + if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == true then + w[i][2] = 0 + end + total = total + w[i][2] + end + local roll = math.random() * total + local acc = 0 + for i = 1, #w do + acc = acc + w[i][2] + if roll <= acc then + node.type = w[i][1] + break + end + end + end + end +end`), + method('IsReachable', `local list +if self.CurrentNodeId == "" then + list = self.MapStart +else + local node = self.MapNodes[self.CurrentNodeId] + if node == nil then + return false + end + list = node.next +end +for i = 1, #list do + if list[i] == id then + return true + end +end +return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'), + method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id +local e = _EntityService:GetEntityByPath(base) +if e == nil then + return +end +local node = self.MapNodes[id] +if node == nil then + e.Enable = false + return +end +e.Enable = true +local ruid = self.NodeIcons[node.type] +if ruid == nil then + ruid = self.NodeIcons["combat"] +end +if e.SpriteGUIRendererComponent ~= nil and ruid ~= nil then + e.SpriteGUIRendererComponent.ImageRUID = ruid +end +local reachable = self:IsReachable(id) +local visited = false +if self.VisitedNodes ~= nil then + for i = 1, #self.VisitedNodes do + if self.VisitedNodes[i] == id then visited = true end + end +end +if e.SpriteGUIRendererComponent ~= nil then + if id == self.CurrentNodeId then + e.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) + elseif visited == true then + e.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.9) + elseif reachable == true then + e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) + else + e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85) + end +end +if e.ButtonComponent ~= nil then + e.ButtonComponent.Enable = reachable +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), + method('RenderMapDots', `local node = self.MapNodes[fromId] +local has = false +if node ~= nil then + for i = 1, #node.next do + if node.next[i] == toId then has = true end + end +end +for k = 1, 3 do + local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k)) + if d ~= nil then + d.Enable = has + if has == true and d.SpriteGUIRendererComponent ~= nil then + if fromId == self.CurrentNodeId then + d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1) + else + d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8) + end + end + end +end`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' }, + ]), + method('RenderMap', `for r = 1, ${MAP_ROWS} do + for c = 1, ${MAP_COLS} do + self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c)) + end +end +self:RenderMapNode("boss") +for r = 1, ${MAP_ROWS} - 1 do + for c = 1, ${MAP_COLS} do + local fid = "r" .. tostring(r) .. "c" .. tostring(c) + for c2 = c - 1, c + 1 do + if c2 >= 1 and c2 <= ${MAP_COLS} then + self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2)) + end + end + end +end +for c = 1, ${MAP_COLS} do + local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) + self:RenderMapDots(fid .. "_b", fid, "boss") +end +`), + method('PickNode', `if self.RunActive ~= true then + return +end +if self:IsReachable(id) ~= true then + return +end +self.CurrentNodeId = id +if self.VisitedNodes == nil then + self.VisitedNodes = {} +end +table.insert(self.VisitedNodes, id) +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud") +if hud ~= nil then + hud.Enable = false +end +local node = self.MapNodes[id] +self.Depth = node.row +self:RenderRun() +if node.type == "shop" then + self:ShowShop() +elseif node.type == "rest" then + self:ShowRest() +elseif node.type == "treasure" then + self:ShowTreasure() +else + self.CurrentEnemyId = "" + self:StartCombat() +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), +]; diff --git a/tools/deck/cb/render.mjs b/tools/deck/cb/render.mjs new file mode 100644 index 0000000..5d4cc10 --- /dev/null +++ b/tools/deck/cb/render.mjs @@ -0,0 +1,308 @@ +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/DefaultGroup/CombatHud/MonsterSlot" .. 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 .. "/TargetFrame", i == shownTarget) + 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/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp)) +self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220) +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) +self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) +local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) +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/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb) +self:RenderRun()`), + method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0)) +local base = "/ui/DefaultGroup/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/DefaultGroup/CombatHud/MonsterSlot" .. 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/DefaultGroup/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/DefaultGroup/CombatHud/MonsterSlot" .. 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/DefaultGroup/CombatHud/TopBar/Floor", floorText) +self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`), +]; diff --git a/tools/deck/cb/reward.mjs b/tools/deck/cb/reward.mjs new file mode 100644 index 0000000..c85f1b5 --- /dev/null +++ b/tools/deck/cb/reward.mjs @@ -0,0 +1,55 @@ +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 rewardMethods = [ + method('CardPool', `local pool = {} +for id, c in pairs(self.Cards) do + if c.token ~= true and (c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob)) then + table.insert(pool, id) + end +end +table.sort(pool) +return pool`, [], 0, 'any'), + method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) +self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) +local pool = self:CardPool() +local byRarity = {} +for _, id in ipairs(pool) do + local r = self.Cards[id].rarity or "normal" + if byRarity[r] == nil then byRarity[r] = {} end + table.insert(byRarity[r], id) +end +self.RewardChoices = {} +for i = 1, 3 do + local roll = math.random(1, 100) + local want = "normal" + if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end + local bucket = byRarity[want] + if bucket == nil or #bucket == 0 then bucket = pool end + self.RewardChoices[i] = bucket[math.random(1, #bucket)] + self:ApplyRewardVisual(i, self.RewardChoices[i]) +end +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud") +if hud ~= nil then + hud.Enable = true +end`), + method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [ + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, + ]), + method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then + return +end +if slot ~= 0 and self.RewardChoices ~= nil then + local id = self.RewardChoices[slot] + if id ~= nil then + table.insert(self.RunDeck, id) + end +end +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud") +if hud ~= nil then + hud.Enable = false +end +self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), +]; diff --git a/tools/deck/cb/run.mjs b/tools/deck/cb/run.mjs new file mode 100644 index 0000000..83f1cc5 --- /dev/null +++ b/tools/deck/cb/run.mjs @@ -0,0 +1,207 @@ +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 runMethods = [ + method('StartRun', `if self.SelectedClass == "magician" then + self.PlayerMaxHp = ${CLASSES.magician.maxHp} +self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} } +elseif self.SelectedClass == "bandit" then + self.PlayerMaxHp = ${CLASSES.bandit.maxHp} + self.RunDeck = { ${CARDS.starterDecks.bandit.map(luaStr).join(', ')} } +else + self.PlayerMaxHp = ${CLASSES.warrior.maxHp} + self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} } +end +self.PlayerMaxHp = self.PlayerMaxHp - self:AscStartHpPenalty() +self.PlayerHp = self.PlayerMaxHp +self.Gold = 0 +self.Floor = 1 +self.RunLength = ${ACT_COUNT} +self.RunActive = true +self.RunRelics = {} +self.RunPotions = {} +self.PotionSlots = ${POTIONS.baseSlots} +${luaPotionsTable(POTIONS.potions)} +${luaRelicsTable(RELICS.relics)} +self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} } +${luaEnemiesTable(ENEMIES.enemies)} +self.CurrentNodeId = "" +self.CurrentEnemyId = "" +self.PlayerJob = "" +${luaJobsTable(JOBS)} +${luaFramesTable()} +${luaNodeIconsTable()} +self:GenerateMap() +self:BindButtons() +self:AddRelic("${RELICS.startingRelic}") +self:ApplySoulUnlocks() +self:RenderPotions() +self:TeleportToActMap() +self:ShowMap()`), + method('KickCombatCamera', `local cam = nil +local lp = _UserService.LocalPlayer +if lp ~= nil then cam = lp.CameraComponent end +if cam == nil then cam = _CameraService:GetCurrentCameraComponent() end +if cam ~= nil then cam.ConfineCameraArea = false end +_TimerService:SetTimerOnce(function() + local cc = nil + local lp2 = _UserService.LocalPlayer + if lp2 ~= nil then cc = lp2.CameraComponent end + if cc == nil then cc = _CameraService:GetCurrentCameraComponent() end + if cc ~= nil then + cc.ZoomRatio = ${CAM.zoomRatio} + cc.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY}) + cc.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY}) + cc.ConfineCameraArea = true + end +end, 0.2)`), + method('StartCombat', `self:ShowState("combat") +self:KickCombatCamera() +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false) +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false) +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false) +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", false) +self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) +self.MaxEnergy = 3 +self.Turn = 0 +self.PlayerBlock = 0 +self.PlayerStr = 0 +self.PlayerDex = 0 +self.PlayerThorns = 0 +self.PlayerWeak = 0 +self.PlayerVuln = 0 +self.PlayerPowers = {} +self.FightAttackCount = 0 +self.DmgPopSeq = 0 +self.FirstHpLossDone = false +self.ClayBlockNext = 0 +self.DiscardSelectRemaining = 0 +self.DiscardSelectTotal = 0 +self.DiscardPostShiv = 0 +self.DiscardShivPerPick = 0 +self.CombatOver = false +self.DiscardPile = {} +self.ExhaustPile = {} +self.Hand = {} +${luaCardsTable(CARDS.cards)} +self.DrawPile = {} +for i = 1, #self.RunDeck do + self.DrawPile[i] = self.RunDeck[i] +end +self:Shuffle(self.DrawPile) +self:BuildMonsters() +self:RenderCombat() +self:StartPlayerTurn() +self:ApplyRelics("combatStart") +self:RenderCombat()`), + method('RegisterMonster', `if self.Registered == nil then + self.Registered = {} +end +local g = group +if g == nil or g == "" then g = "combat" end +local mp = mapName +if mp == nil then mp = "" end +table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [ + { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' }, + ]), + method('BuildMonsters', `self.Monsters = {} +local g = "combat" +local node = self.MapNodes[self.CurrentNodeId] +if node ~= nil and node.type ~= nil then g = node.type end +local pmap = "" +local lp = _UserService.LocalPlayer +if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end +local reg = self.Registered or {} +for i = 1, #reg do + if reg[i].entity ~= nil and isvalid(reg[i].entity) then + reg[i].entity:SetVisible(false) + end +end +local byGroup = {} +for i = 1, #reg do + local r = reg[i] + if r.entity ~= nil and isvalid(r.entity) and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then + local gg = r.group + if gg == nil or gg == "" then gg = "combat" end + if byGroup[gg] == nil then byGroup[gg] = {} end + local x = 0 + if r.entity.TransformComponent ~= nil then + x = r.entity.TransformComponent.WorldPosition.x + end + table.insert(byGroup[gg], { entity = r.entity, enemyId = r.enemyId, x = x }) + end +end +-- 노드 타입별 랜덤 구성: 일반 1~3 / 엘리트 1+일반0~2 / 보스 1 +local chosen = {} +local function takeFrom(key, k) + local src = byGroup[key] or {} + local pool = {} + for i = 1, #src do pool[i] = src[i] end + self:Shuffle(pool) + local taken = 0 + for i = 1, #pool do + if taken >= k then break end + table.insert(chosen, pool[i]) + taken = taken + 1 + end +end +if g == "boss" then + takeFrom("boss", 1) +elseif g == "elite" then + takeFrom("elite", 1) + takeFrom("combat", math.random(0, 2)) +else + takeFrom("combat", math.random(1, 3)) +end +if #chosen == 0 then takeFrom(g, 1) end +if #chosen == 0 then takeFrom("combat", 1) end +table.sort(chosen, function(a, b) return a.x < b.x end) +local mult = 1 + (self.Floor - 1) * 0.45 +if g == "elite" or g == "boss" then + mult = mult + self:AscEliteBonus() +end +local n = #chosen +if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end +for i = 1, n do + local item = chosen[i] + local e = self.Enemies[item.enemyId] + if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end + local intents = {} + for k = 1, #e.intents do + local v = e.intents[k].value or 0 + if e.intents[k].kind == "Attack" then + v = math.floor(v * mult * self:AscAtkMult()) + elseif e.intents[k].kind ~= "Debuff" then + v = math.floor(v * mult) + end + intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect, card = e.intents[k].card, count = e.intents[k].count } + end + local maxHp = math.floor(e.maxHp * mult * self:AscHpMult()) + local hitClip = nil + local standClip = nil + if item.entity.StateAnimationComponent ~= nil then + pcall(function() + hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"] + standClip = item.entity.StateAnimationComponent.ActionSheet["stand"] + end) + end + local startIdx = 1 + if #intents > 0 then startIdx = math.random(1, #intents) end + self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name, + hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0, + hitClip = hitClip, standClip = standClip, motionBusy = false, + intents = intents, intentIdx = startIdx, alive = true, slot = i } + self:ReviveMonsterEntity(item.entity) + self:PositionMonsterSlot(i) +end +self.TargetIndex = 1`), + method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then + return +end +monster:SetEnable(true) +monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]), +]; diff --git a/tools/deck/cb/runend.mjs b/tools/deck/cb/runend.mjs new file mode 100644 index 0000000..66bf74e --- /dev/null +++ b/tools/deck/cb/runend.mjs @@ -0,0 +1,37 @@ +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/DefaultGroup/CombatHud/Result", text) +local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/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/shop.mjs b/tools/deck/cb/shop.mjs new file mode 100644 index 0000000..9556148 --- /dev/null +++ b/tools/deck/cb/shop.mjs @@ -0,0 +1,173 @@ +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 shopMethods = [ + method('ShowShop', `local pool = self:CardPool() +self.ShopChoices = {} +self.ShopBought = { false, false, false } +for i = 1, 3 do + self.ShopChoices[i] = pool[math.random(1, #pool)] +end +self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)] +self.ShopRelicBought = false +local pkeys = {} +for pid, _ in pairs(self.Potions) do + table.insert(pkeys, pid) +end +table.sort(pkeys) +self.ShopPotion = pkeys[math.random(1, #pkeys)] +self.ShopPotionBought = false +self:RenderShop() +self:ShowState("shop")`), + method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold)) +for i = 1, 3 do + local cid = self.ShopChoices[i] + local c = self.Cards[cid] + local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i) + if c ~= nil then + self:ApplyCardFace(base, cid) + self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소") + local e = _EntityService:GetEntityByPath(base) + if e ~= nil and e.SpriteGUIRendererComponent ~= nil then + if self.ShopBought[i] == true then + e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) + end + end + end +end +local rr = self.Relics[self.ShopRelic] +if rr ~= nil then + self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc) + self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소") + local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic") + if re ~= nil and re.SpriteGUIRendererComponent ~= nil then + if self.ShopRelicBought == true then + re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) + else + re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1) + end + end +end +local pp = self.Potions[self.ShopPotion] +if pp ~= nil then + self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc) + self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소") + local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion") + if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then + if self.ShopPotionBought == true then + pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) + else + pe.SpriteGUIRendererComponent.Color = Color(0.45, 0.7, 0.55, 1) + end + end +end`), + method('BuyRelic', `if self.ShopRelicBought == true then + return +end +if self.Gold < ${RELIC_PRICE} then + return +end +self.Gold = self.Gold - ${RELIC_PRICE} +self:AddRelic(self.ShopRelic) +self.ShopRelicBought = true +self:RenderShop() +self:RenderRun()`), + method('BuyPotion', `if self.ShopPotionBought == true then + return +end +if self.Gold < ${POTIONS.shopPrice} then + return +end +if self.RunPotions ~= nil and #self.RunPotions >= self.PotionSlots then + self:Toast("물약 슬롯이 가득 찼습니다") + return +end +if self:AddPotion(self.ShopPotion) == true then + self.Gold = self.Gold - ${POTIONS.shopPrice} + self.ShopPotionBought = true +end +self:RenderShop() +self:RenderRun()`), + method('BuyCard', `if self.ShopBought == nil or self.ShopBought[slot] == true then + return +end +if self.Gold < ${CARD_PRICE} then + return +end +self.Gold = self.Gold - ${CARD_PRICE} +table.insert(self.RunDeck, self.ShopChoices[slot]) +self.ShopBought[slot] = true +self:RenderShop() +self:RenderRun()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('ShowRest', `local old = self.PlayerHp +self.PlayerHp = self.PlayerHp + ${REST_HEAL} +if self.PlayerHp > self.PlayerMaxHp then + self.PlayerHp = self.PlayerMaxHp +end +local healed = self.PlayerHp - old +self:SetText("/ui/DefaultGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")") +self:RenderCombat() +self:ShowState("rest")`), + method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud") +if s ~= nil then + s.Enable = false +end +local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud") +if r ~= nil then + r.Enable = false +end +local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud") +if t ~= nil then + t.Enable = false +end +self:ShowMap()`), + method('ShowTreasure', `self.ChestOpened = false +local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") +if chest ~= nil then + if chest.SpriteGUIRendererComponent ~= nil then + chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}" + end + if chest.UITransformComponent ~= nil then + chest.UITransformComponent.anchoredPosition = Vector2(0, 40) + end +end +self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false) +self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true) +self:ShowState("treasure")`), + method('OpenChest', `if self.ChestOpened == true then + return +end +self.ChestOpened = true +self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false) +local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") +local steps = { 10, -10, 8, -8, 5, 0 } +for i = 1, #steps do + local dx = steps[i] + _TimerService:SetTimerOnce(function() + if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then + chest.UITransformComponent.anchoredPosition = Vector2(dx, 40) + end + end, 0.08 * i) +end +_TimerService:SetTimerOnce(function() + if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then + chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}" + end + local g = 40 + math.random(0, 20) + local nid = self:PickNewRelic() + local msg = "" + if nid ~= "" then + self:AddRelic(nid) + local nr = self.Relics[nid] + msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g) + else + g = g + 30 + msg = "메소 +" .. tostring(g) + end + self.Gold = self.Gold + g + self:RenderRun() + self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg) + self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true) +end, 0.55)`), +]; diff --git a/tools/deck/cb/soul.mjs b/tools/deck/cb/soul.mjs new file mode 100644 index 0000000..ff9cc70 --- /dev/null +++ b/tools/deck/cb/soul.mjs @@ -0,0 +1,114 @@ +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 soulMethods = [ + method('ShowSoulShop', `self:RenderSoulLabel() +self:RenderSoulShop() +self:BindSoulShopButtons() +self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`), + method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`), + method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId) +local e1, pts = ds:GetAndWait("soulPoints") +local e2, unl = ds:GetAndWait("soulUnlocks") +local p = 0 +if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end +local u = "" +if e2 == 0 and unl ~= nil then u = unl end +self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5), + method('RecvSouls', `self.SoulPoints = p +self.SoulUnlocks = {} +if u ~= nil and u ~= "" then + for key in string.gmatch(u, "([^,]+)") do + self.SoulUnlocks[key] = true + end +end +self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6), + method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId) +ds:SetAndWait("soulPoints", tostring(p)) +ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5), + method('SerializeUnlocks', `local parts = {} +if self.SoulUnlocks ~= nil then + for k, v in pairs(self.SoulUnlocks) do + if v == true then table.insert(parts, k) end + end +end +return table.concat(parts, ",")`, [], 0, 'string'), + method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n +local lp = _UserService.LocalPlayer +if lp ~= nil then + self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) +end +self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]), + method('BuySoulUnlock', `local d = nil +if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end +if d == nil then return end +if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then + self:Toast("이미 보유 중입니다") + return +end +if (self.SoulPoints or 0) < d.cost then + self:Toast("영혼이 부족합니다") + return +end +self.SoulPoints = self.SoulPoints - d.cost +if self.SoulUnlocks == nil then self.SoulUnlocks = {} end +self.SoulUnlocks[d.key] = true +local lp = _UserService.LocalPlayer +if lp ~= nil then + self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) +end +self:Toast(d.name .. " 해금!") +self:RenderSoulLabel() +self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]), + method('RenderSoulShop', `local defs = self.SoulShopDef or {} +for i = 1, 4 do + local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i) + local d = defs[i] + if d == nil then + self:SetEntityEnabled(base, false) + else + self:SetEntityEnabled(base, true) + self:SetText(base .. "/Name", d.name) + self:SetText(base .. "/Desc", d.desc) + local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true + if owned then + self:SetText(base .. "/Status", "보유 중") + elseif (self.SoulPoints or 0) >= d.cost then + self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매") + else + self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족") + end + end +end`), + method('BindSoulShopButtons', `if self.SoulShopBound == true then + return +end +self.SoulShopBound = true +for i = 1, 4 do + local idx = i + local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)) + if e ~= nil and e.ButtonComponent ~= nil then + e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end) + end +end`), + method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end +if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end +if self.SoulUnlocks["hp"] == true then + self.PlayerMaxHp = self.PlayerMaxHp + 15 + self.PlayerHp = self.PlayerMaxHp +end +if self.SoulUnlocks["trim"] == true then + for i = 1, #self.RunDeck do + local cid = self.RunDeck[i] + if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then + table.remove(self.RunDeck, i) + break + end + end +end +if self.SoulUnlocks["relic"] == true then + local nid = self:PickNewRelic() + if nid ~= "" then self:AddRelic(nid) end +end`), +]; diff --git a/tools/deck/cb/state.mjs b/tools/deck/cb/state.mjs new file mode 100644 index 0000000..a21f04c --- /dev/null +++ b/tools/deck/cb/state.mjs @@ -0,0 +1,193 @@ +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 stateMethods = [ + method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false) +self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false) +self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false) +self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) +self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`), + method('ShowState', `self:HideGameHud() +self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu") +self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect") +self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby") +if state == "map" then + self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true) +elseif state == "combat" then + self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true) + self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true) + self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true) +elseif state == "shop" then + self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true) +elseif state == "rest" then + self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true) +elseif state == "treasure" then + self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true) +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]), + method('ShowMainMenu', `self.SelectedClass = "" +self:RenderAscension() +self:ShowState("menu") +self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처") +self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요") +self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임") +self:BindMenuButtons()`), + method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton") +if buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then + if self.NewGameHandler ~= nil then + buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler) + self.NewGameHandler = nil + end + self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end) +end +local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton") +if warrior ~= nil and warrior.ButtonComponent ~= nil then + if self.WarriorSelectHandler ~= nil then + warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler) + self.WarriorSelectHandler = nil + end + self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end) +end +local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton") +if thief ~= nil and thief.ButtonComponent ~= nil then + if self.ThiefSelectHandler ~= nil then + thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler) + self.ThiefSelectHandler = nil + end + self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end) +end +local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton") +if mage ~= nil and mage.ButtonComponent ~= nil then + if self.MageSelectHandler ~= nil then + mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler) + self.MageSelectHandler = nil + end + self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end) +end +local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") +if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then + if self.AllDeckCloseHandler ~= nil then + allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) + self.AllDeckCloseHandler = nil + end + self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) +end +self:BindClassDeckTabs() +local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton") +if start ~= nil and start.ButtonComponent ~= nil then + if self.StartGameHandler ~= nil then + start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler) + self.StartGameHandler = nil + end + self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end) +end +local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton") +if charBack ~= nil and charBack.ButtonComponent ~= nil then + if self.CharBackHandler ~= nil then + charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler) + self.CharBackHandler = nil + end + self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end) +end +local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus") +if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then + if self.AscMinusHandler ~= nil then + ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler) + self.AscMinusHandler = nil + end + self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end) +end +local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus") +if ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then + if self.AscPlusHandler ~= nil then + ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler) + self.AscPlusHandler = nil + end + self.AscPlusHandler = ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end) +end`), + method('ShowLobby', `self.SelectedClass = "" +self:RenderAscension() +self:RenderSoulLabel() +self:ShowState("lobby") +self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/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/DefaultGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s)) +self:SetText("/ui/DefaultGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`), + 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 then + e:ConnectEvent(ButtonClickEvent, fn) + end +end +bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end) +bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end) +bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end) +bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`), + method('ShowCodex', `self.CodexMode = true +self.ClassDeckMode = true +local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") +if close ~= nil and close.ButtonComponent ~= nil then + if self.AllDeckCloseHandler ~= nil then + close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) + end + self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) +end +self:BindClassDeckTabs() +self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) +self:SetClassDeckTab("warrior") +local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") +if hud ~= nil then + hud.Enable = true +end +self:RenderAllDeck()`), + method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`), + method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`), +]; diff --git a/tools/deck/cb/tooltip.mjs b/tools/deck/cb/tooltip.mjs new file mode 100644 index 0000000..237dbfb --- /dev/null +++ b/tools/deck/cb/tooltip.mjs @@ -0,0 +1,115 @@ +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 tooltipMethods = [ + method('BuildCardKeywordTooltip', `if c == nil then + return "" +end +local lines = {} +local function add(name, desc) + for i = 1, #lines do + if string.find(lines[i], name .. ":", 1, true) == 1 then + return + end + end + table.insert(lines, name .. ": " .. desc) +end +local cardDesc = c.desc or "" +if c.sly == true or string.find(cardDesc, "교활", 1, true) ~= nil then + add("교활", "버려지면 비용 없이 사용됩니다.") +end +if c.retain == true or string.find(cardDesc, "보존", 1, true) ~= nil then + add("보존", "턴 종료 시 버려지지 않고 손에 남습니다.") +end +if c.dex ~= nil and c.dex > 0 or string.find(cardDesc, "민첩", 1, true) ~= nil then + add("민첩", "카드로 얻는 방어도가 증가합니다.") +end +if c.thorns ~= nil and c.thorns > 0 or string.find(cardDesc, "가시", 1, true) ~= nil then + add("가시", "피해를 받으면 공격자에게 반사 피해를 줍니다.") +end +if c.exhaust == true or string.find(cardDesc, "소멸.", 1, true) ~= nil then + add("소멸", "사용 후 소멸 덱으로 이동해 이번 전투 동안 다시 나오지 않습니다.") +end +if string.find(cardDesc, "선천성", 1, true) ~= nil then + add("선천성", "전투 시작 시 손패에 들어옵니다.") +end +if c.vuln ~= nil and c.vuln > 0 then + add("취약", "받는 공격 피해가 50% 증가합니다.") +end +if c.weak ~= nil and c.weak > 0 then + add("약화", "주는 공격 피해가 25% 감소합니다.") +end +if c.poison ~= nil and c.poison > 0 then + add("중독", "턴 시작 시 체력을 잃고 수치가 1 감소합니다.") +end +if c.pierce == true then + add("관통", "방어도를 무시하고 피해를 줍니다.") +end +if c.aoe == true then + add("전체", "모든 적에게 적용됩니다.") +end +if c.kind == "Power" then + add("파워", "사용하면 전투 동안 지속 효과로 남습니다.") +end +if c.unplayable == true then + add("저주", "사용할 수 없고 손패를 방해합니다.") +end +local out = "" +for i = 1, #lines do + if i > 1 then out = out .. "\\n" end + out = out .. lines[i] +end +return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'), + method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then + return +end +local cardId = self.Hand[slot] +if cardId == nil then + return +end +local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) +local tx = 0 +if e ~= nil and e.UITransformComponent ~= nil then + tx = e.UITransformComponent.anchoredPosition.x + e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1) +end +local c = self.Cards[cardId] +if c ~= nil then + local tip = self:BuildCardKeywordTooltip(c) + if tip ~= "" then + local tipX = tx + 270 + if tx > 180 then tipX = tx - 270 end + if tipX > 760 then tipX = tx - 270 end + if tipX < -760 then tipX = tx + 270 end + self:ShowTooltipAt("키워드", tip, tipX, 90) + else + self:HideTooltip() + end +end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) +if e ~= nil and e.UITransformComponent ~= nil then + e.UITransformComponent.UIScale = Vector3(1, 1, 1) +end +self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), + method('ShowTooltip', `self:ShowTooltipAt(name, desc, x, 400)`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, + ]), + method('ShowTooltipAt', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name) +self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc) +local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox") +if e ~= nil then + if e.UITransformComponent ~= nil then + e.UITransformComponent.anchoredPosition = Vector2(x, y) + end + e.Enable = true +end`, [ + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' }, + { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, + { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' }, + ]), + method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`), +]; diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 264cdda..e91186d 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -2,6 +2,23 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from './lib/data.mjs'; import { prop, method, codeblock, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from './lib/codeblock.mjs'; +import { bootMethods } from './cb/boot.mjs'; +import { stateMethods } from './cb/state.mjs'; +import { soulMethods } from './cb/soul.mjs'; +import { charSelectMethods } from './cb/charselect.mjs'; +import { runMethods } from './cb/run.mjs'; +import { deckTurnMethods } from './cb/deckturn.mjs'; +import { deckViewMethods } from './cb/deckview.mjs'; +import { handMethods } from './cb/hand.mjs'; +import { combatMethods } from './cb/combat.mjs'; +import { jobMethods } from './cb/jobs.mjs'; +import { runEndMethods } from './cb/runend.mjs'; +import { renderMethods } from './cb/render.mjs'; +import { rewardMethods } from './cb/reward.mjs'; +import { itemMethods } from './cb/items.mjs'; +import { tooltipMethods } from './cb/tooltip.mjs'; +import { mapMethods } from './cb/map.mjs'; +import { shopMethods } from './cb/shop.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 { buildDeckHud } from './hud/deckhud.mjs'; import { buildDeckInspect } from './hud/deckinspect.mjs'; @@ -346,3213 +363,23 @@ function writeCodeblocks() { prop('number', 'DiscardPostShiv', '0'), prop('number', 'DiscardShivPerPick', '0'), ], [ - method('OnBeginPlay', `${luaCardsTable(CARDS.cards)} -${luaFramesTable()} -${luaNodeIconsTable()} -${luaSoulShopTable(SOUL_UNLOCKS)} -self.SoulUnlocks = {} -self.SoulPoints = self.SoulPoints or 0 -self:ShowLobby() -local lp = _UserService.LocalPlayer -if lp ~= nil then - self:ReqLoadAscension(lp.PlayerComponent.UserId) - self:ReqLoadSouls(lp.PlayerComponent.UserId) -end -_InputService:ConnectEvent(KeyDownEvent, function(e) - if e.key == KeyboardKey.LeftControl then - local lp2 = _UserService.LocalPlayer - if lp2 ~= nil and lp2.CurrentMapName == "${LOBBY_MAP}" and self.RunActive ~= true then - self:PlayerAttackMotion() - end - end -end)`), - method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId) -local errCode, value = ds:GetAndWait("ascensionUnlocked") -local n = 0 -if errCode == 0 and value ~= nil and value ~= "" then - n = tonumber(value) or 0 -end -self:RecvAscension(n, userId)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }], 5), - method('RecvAscension', `self.AscensionUnlocked = n -if self.AscensionLevel > self.AscensionUnlocked then - self.AscensionLevel = self.AscensionUnlocked -end -self:RenderAscension()`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }, - ], 6), - method('SaveAscension', `local ds = _DataStorageService:GetUserDataStorage(userId) -ds:SetAndWait("ascensionUnlocked", tostring(n))`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'userId' }, - ], 5), - method('AdjustAscension', `local v = self.AscensionLevel + delta -if v < 0 then v = 0 end -if v > self.AscensionUnlocked then v = self.AscensionUnlocked end -self.AscensionLevel = v -self:RenderAscension()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'delta' }]), - method('RenderAscension', `self:SetText("/ui/DefaultGroup/MainMenu/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked)) -self:SetText("/ui/DefaultGroup/LobbyHud/AscLabel", "승천 " .. string.format("%d", self.AscensionLevel) .. " / 해금 " .. string.format("%d", self.AscensionUnlocked))`), - method('AscHpMult', `local m = 1 -if self.AscensionLevel >= 1 then m = m + 0.1 end -if self.AscensionLevel >= 6 then m = m + 0.1 end -return m`, [], 0, 'number'), - method('AscAtkMult', `local m = 1 -if self.AscensionLevel >= 2 then m = m + 0.1 end -if self.AscensionLevel >= 7 then m = m + 0.1 end -return m`, [], 0, 'number'), - method('AscEliteBonus', `local b = 0 -if self.AscensionLevel >= 4 then b = b + 0.2 end -if self.AscensionLevel >= 9 then b = b + 0.2 end -return b`, [], 0, 'number'), - method('AscGoldMult', `local m = 1 -if self.AscensionLevel >= 5 then m = m - 0.25 end -if self.AscensionLevel >= 10 then m = m - 0.25 end -return m`, [], 0, 'number'), - method('AscStartHpPenalty', `local p = 0 -if self.AscensionLevel >= 3 then p = p + 10 end -if self.AscensionLevel >= 8 then p = p + 10 end -return p`, [], 0, 'number'), - method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false) -self:SetEntityEnabled("/ui/DefaultGroup/Button_Jump", false) -self:SetEntityEnabled("/ui/DefaultGroup/UIJoystick", false) -self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`), - method('ShowState', `self:HideGameHud() -self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", state == "menu") -self:SetEntityEnabled("/ui/DefaultGroup/CharacterSelectHud", state == "charselect") -self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", state == "lobby") -if state == "map" then - self:SetEntityEnabled("/ui/DefaultGroup/MapHud", true) -elseif state == "combat" then - self:SetEntityEnabled("/ui/DefaultGroup/CombatHud", true) - self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true) - self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true) -elseif state == "shop" then - self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true) -elseif state == "rest" then - self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true) -elseif state == "treasure" then - self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true) -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]), - method('ShowMainMenu', `self.SelectedClass = "" -self:RenderAscension() -self:ShowState("menu") -self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처") -self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요") -self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임") -self:BindMenuButtons()`), - method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton") -if buttonEntity ~= nil and buttonEntity.ButtonComponent ~= nil then - if self.NewGameHandler ~= nil then - buttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler) - self.NewGameHandler = nil - end - self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, function() self:ShowCharacterSelect() end) -end -local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton") -if warrior ~= nil and warrior.ButtonComponent ~= nil then - if self.WarriorSelectHandler ~= nil then - warrior:DisconnectEvent(ButtonClickEvent, self.WarriorSelectHandler) - self.WarriorSelectHandler = nil - end - self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end) -end -local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton") -if thief ~= nil and thief.ButtonComponent ~= nil then - if self.ThiefSelectHandler ~= nil then - thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler) - self.ThiefSelectHandler = nil - end - self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end) -end -local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton") -if mage ~= nil and mage.ButtonComponent ~= nil then - if self.MageSelectHandler ~= nil then - mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler) - self.MageSelectHandler = nil - end - self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end) -end -local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") -if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then - if self.AllDeckCloseHandler ~= nil then - allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) - self.AllDeckCloseHandler = nil - end - self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) -end -self:BindClassDeckTabs() -local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton") -if start ~= nil and start.ButtonComponent ~= nil then - if self.StartGameHandler ~= nil then - start:DisconnectEvent(ButtonClickEvent, self.StartGameHandler) - self.StartGameHandler = nil - end - self.StartGameHandler = start:ConnectEvent(ButtonClickEvent, function() self:StartNewGame() end) -end -local charBack = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/BackButton") -if charBack ~= nil and charBack.ButtonComponent ~= nil then - if self.CharBackHandler ~= nil then - charBack:DisconnectEvent(ButtonClickEvent, self.CharBackHandler) - self.CharBackHandler = nil - end - self.CharBackHandler = charBack:ConnectEvent(ButtonClickEvent, function() self:ShowLobby() end) -end -local ascMinus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscMinus") -if ascMinus ~= nil and ascMinus.ButtonComponent ~= nil then - if self.AscMinusHandler ~= nil then - ascMinus:DisconnectEvent(ButtonClickEvent, self.AscMinusHandler) - self.AscMinusHandler = nil - end - self.AscMinusHandler = ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end) -end -local ascPlus = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/AscPlus") -if ascPlus ~= nil and ascPlus.ButtonComponent ~= nil then - if self.AscPlusHandler ~= nil then - ascPlus:DisconnectEvent(ButtonClickEvent, self.AscPlusHandler) - self.AscPlusHandler = nil - end - self.AscPlusHandler = ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end) -end`), - method('ShowLobby', `self.SelectedClass = "" -self:RenderAscension() -self:RenderSoulLabel() -self:ShowState("lobby") -self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/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/DefaultGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", s)) -self:SetText("/ui/DefaultGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", s))`), - 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 then - e:ConnectEvent(ButtonClickEvent, fn) - end -end -bindClick("/ui/DefaultGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end) -bindClick("/ui/DefaultGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end) -bindClick("/ui/DefaultGroup/BoardHud/Close", function() self:CloseBoard() end) -bindClick("/ui/DefaultGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`), - method('ShowCodex', `self.CodexMode = true -self.ClassDeckMode = true -local close = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") -if close ~= nil and close.ButtonComponent ~= nil then - if self.AllDeckCloseHandler ~= nil then - close:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) - end - self.AllDeckCloseHandler = close:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) -end -self:BindClassDeckTabs() -self:SetEntityEnabled("/ui/DefaultGroup/LobbyHud", false) -self:SetClassDeckTab("warrior") -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") -if hud ~= nil then - hud.Enable = true -end -self:RenderAllDeck()`), - method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`), - method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`), - method('ShowSoulShop', `self:RenderSoulLabel() -self:RenderSoulShop() -self:BindSoulShopButtons() -self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`), - method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`), - method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId) -local e1, pts = ds:GetAndWait("soulPoints") -local e2, unl = ds:GetAndWait("soulUnlocks") -local p = 0 -if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end -local u = "" -if e2 == 0 and unl ~= nil then u = unl end -self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5), - method('RecvSouls', `self.SoulPoints = p -self.SoulUnlocks = {} -if u ~= nil and u ~= "" then - for key in string.gmatch(u, "([^,]+)") do - self.SoulUnlocks[key] = true - end -end -self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6), - method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId) -ds:SetAndWait("soulPoints", tostring(p)) -ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5), - method('SerializeUnlocks', `local parts = {} -if self.SoulUnlocks ~= nil then - for k, v in pairs(self.SoulUnlocks) do - if v == true then table.insert(parts, k) end - end -end -return table.concat(parts, ",")`, [], 0, 'string'), - method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n -local lp = _UserService.LocalPlayer -if lp ~= nil then - self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) -end -self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]), - method('BuySoulUnlock', `local d = nil -if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end -if d == nil then return end -if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then - self:Toast("이미 보유 중입니다") - return -end -if (self.SoulPoints or 0) < d.cost then - self:Toast("영혼이 부족합니다") - return -end -self.SoulPoints = self.SoulPoints - d.cost -if self.SoulUnlocks == nil then self.SoulUnlocks = {} end -self.SoulUnlocks[d.key] = true -local lp = _UserService.LocalPlayer -if lp ~= nil then - self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) -end -self:Toast(d.name .. " 해금!") -self:RenderSoulLabel() -self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]), - method('RenderSoulShop', `local defs = self.SoulShopDef or {} -for i = 1, 4 do - local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i) - local d = defs[i] - if d == nil then - self:SetEntityEnabled(base, false) - else - self:SetEntityEnabled(base, true) - self:SetText(base .. "/Name", d.name) - self:SetText(base .. "/Desc", d.desc) - local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true - if owned then - self:SetText(base .. "/Status", "보유 중") - elseif (self.SoulPoints or 0) >= d.cost then - self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매") - else - self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족") - end - end -end`), - method('BindSoulShopButtons', `if self.SoulShopBound == true then - return -end -self.SoulShopBound = true -for i = 1, 4 do - local idx = i - local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)) - if e ~= nil and e.ButtonComponent ~= nil then - e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end) - end -end`), - method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end -if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end -if self.SoulUnlocks["hp"] == true then - self.PlayerMaxHp = self.PlayerMaxHp + 15 - self.PlayerHp = self.PlayerMaxHp -end -if self.SoulUnlocks["trim"] == true then - for i = 1, #self.RunDeck do - local cid = self.RunDeck[i] - if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then - table.remove(self.RunDeck, i) - break - end - end -end -if self.SoulUnlocks["relic"] == true then - local nid = self:PickNewRelic() - if nid ~= "" then self:AddRelic(nid) end -end`), - method('ShowCharacterSelect', `self.SelectedClass = "" -self:ShowState("charselect") -self:RenderCharacterSelect()`), - method('SelectClass', `self.SelectedClass = className -self:RenderCharacterSelect()`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }, - ]), - method('RenderCharacterSelect', `local warrior = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/WarriorButton") -if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then - if self.SelectedClass == "warrior" then - warrior.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) - else - warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) - end -end -local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton") -if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then - if self.SelectedClass == "magician" then - mage.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) - else - mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) - end -end -local thief = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/ThiefButton") -if thief ~= nil and thief.SpriteGUIRendererComponent ~= nil then - if self.SelectedClass == "bandit" then - thief.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) - else - thief.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1) - end -end -if self.SelectedClass == "warrior" then - self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨") -elseif self.SelectedClass == "bandit" then - self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "도적 선택됨") -elseif self.SelectedClass == "magician" then - self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨") -else - self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요") -end`), - method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then - self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요") - return -end -self:StartRun()`), - method('SetEntityEnabled', `local e = _EntityService:GetEntityByPath(path) -if e ~= nil then - e.Enable = enabled -end`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' }, - ]), - method('StartRun', `if self.SelectedClass == "magician" then - self.PlayerMaxHp = ${CLASSES.magician.maxHp} -self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} } -elseif self.SelectedClass == "bandit" then - self.PlayerMaxHp = ${CLASSES.bandit.maxHp} - self.RunDeck = { ${CARDS.starterDecks.bandit.map(luaStr).join(', ')} } -else - self.PlayerMaxHp = ${CLASSES.warrior.maxHp} - self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} } -end -self.PlayerMaxHp = self.PlayerMaxHp - self:AscStartHpPenalty() -self.PlayerHp = self.PlayerMaxHp -self.Gold = 0 -self.Floor = 1 -self.RunLength = ${ACT_COUNT} -self.RunActive = true -self.RunRelics = {} -self.RunPotions = {} -self.PotionSlots = ${POTIONS.baseSlots} -${luaPotionsTable(POTIONS.potions)} -${luaRelicsTable(RELICS.relics)} -self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} } -${luaEnemiesTable(ENEMIES.enemies)} -self.CurrentNodeId = "" -self.CurrentEnemyId = "" -self.PlayerJob = "" -${luaJobsTable(JOBS)} -${luaFramesTable()} -${luaNodeIconsTable()} -self:GenerateMap() -self:BindButtons() -self:AddRelic("${RELICS.startingRelic}") -self:ApplySoulUnlocks() -self:RenderPotions() -self:TeleportToActMap() -self:ShowMap()`), - method('KickCombatCamera', `local cam = nil -local lp = _UserService.LocalPlayer -if lp ~= nil then cam = lp.CameraComponent end -if cam == nil then cam = _CameraService:GetCurrentCameraComponent() end -if cam ~= nil then cam.ConfineCameraArea = false end -_TimerService:SetTimerOnce(function() - local cc = nil - local lp2 = _UserService.LocalPlayer - if lp2 ~= nil then cc = lp2.CameraComponent end - if cc == nil then cc = _CameraService:GetCurrentCameraComponent() end - if cc ~= nil then - cc.ZoomRatio = ${CAM.zoomRatio} - cc.CameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY}) - cc.ScreenOffset = Vector2(${CAM.screenOffsetX}, ${CAM.screenOffsetY}) - cc.ConfineCameraArea = true - end -end, 0.2)`), - method('StartCombat', `self:ShowState("combat") -self:KickCombatCamera() -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false) -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false) -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false) -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/DiscardPrompt", false) -self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) -self.MaxEnergy = 3 -self.Turn = 0 -self.PlayerBlock = 0 -self.PlayerStr = 0 -self.PlayerDex = 0 -self.PlayerThorns = 0 -self.PlayerWeak = 0 -self.PlayerVuln = 0 -self.PlayerPowers = {} -self.FightAttackCount = 0 -self.DmgPopSeq = 0 -self.FirstHpLossDone = false -self.ClayBlockNext = 0 -self.DiscardSelectRemaining = 0 -self.DiscardSelectTotal = 0 -self.DiscardPostShiv = 0 -self.DiscardShivPerPick = 0 -self.CombatOver = false -self.DiscardPile = {} -self.ExhaustPile = {} -self.Hand = {} -${luaCardsTable(CARDS.cards)} -self.DrawPile = {} -for i = 1, #self.RunDeck do - self.DrawPile[i] = self.RunDeck[i] -end -self:Shuffle(self.DrawPile) -self:BuildMonsters() -self:RenderCombat() -self:StartPlayerTurn() -self:ApplyRelics("combatStart") -self:RenderCombat()`), - method('RegisterMonster', `if self.Registered == nil then - self.Registered = {} -end -local g = group -if g == nil or g == "" then g = "combat" end -local mp = mapName -if mp == nil then mp = "" end -table.insert(self.Registered, { entity = monster, enemyId = enemyId, group = g, map = mp })`, [ - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'group' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'mapName' }, - ]), - method('BuildMonsters', `self.Monsters = {} -local g = "combat" -local node = self.MapNodes[self.CurrentNodeId] -if node ~= nil and node.type ~= nil then g = node.type end -local pmap = "" -local lp = _UserService.LocalPlayer -if lp ~= nil and lp.CurrentMapName ~= nil then pmap = lp.CurrentMapName end -local reg = self.Registered or {} -for i = 1, #reg do - if reg[i].entity ~= nil and isvalid(reg[i].entity) then - reg[i].entity:SetVisible(false) - end -end -local byGroup = {} -for i = 1, #reg do - local r = reg[i] - if r.entity ~= nil and isvalid(r.entity) and (r.map == nil or r.map == "" or pmap == "" or r.map == pmap) then - local gg = r.group - if gg == nil or gg == "" then gg = "combat" end - if byGroup[gg] == nil then byGroup[gg] = {} end - local x = 0 - if r.entity.TransformComponent ~= nil then - x = r.entity.TransformComponent.WorldPosition.x - end - table.insert(byGroup[gg], { entity = r.entity, enemyId = r.enemyId, x = x }) - end -end --- 노드 타입별 랜덤 구성: 일반 1~3 / 엘리트 1+일반0~2 / 보스 1 -local chosen = {} -local function takeFrom(key, k) - local src = byGroup[key] or {} - local pool = {} - for i = 1, #src do pool[i] = src[i] end - self:Shuffle(pool) - local taken = 0 - for i = 1, #pool do - if taken >= k then break end - table.insert(chosen, pool[i]) - taken = taken + 1 - end -end -if g == "boss" then - takeFrom("boss", 1) -elseif g == "elite" then - takeFrom("elite", 1) - takeFrom("combat", math.random(0, 2)) -else - takeFrom("combat", math.random(1, 3)) -end -if #chosen == 0 then takeFrom(g, 1) end -if #chosen == 0 then takeFrom("combat", 1) end -table.sort(chosen, function(a, b) return a.x < b.x end) -local mult = 1 + (self.Floor - 1) * 0.45 -if g == "elite" or g == "boss" then - mult = mult + self:AscEliteBonus() -end -local n = #chosen -if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end -for i = 1, n do - local item = chosen[i] - local e = self.Enemies[item.enemyId] - if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end - local intents = {} - for k = 1, #e.intents do - local v = e.intents[k].value or 0 - if e.intents[k].kind == "Attack" then - v = math.floor(v * mult * self:AscAtkMult()) - elseif e.intents[k].kind ~= "Debuff" then - v = math.floor(v * mult) - end - intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect, card = e.intents[k].card, count = e.intents[k].count } - end - local maxHp = math.floor(e.maxHp * mult * self:AscHpMult()) - local hitClip = nil - local standClip = nil - if item.entity.StateAnimationComponent ~= nil then - pcall(function() - hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"] - standClip = item.entity.StateAnimationComponent.ActionSheet["stand"] - end) - end - local startIdx = 1 - if #intents > 0 then startIdx = math.random(1, #intents) end - self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name, - hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0, - hitClip = hitClip, standClip = standClip, motionBusy = false, - intents = intents, intentIdx = startIdx, alive = true, slot = i } - self:ReviveMonsterEntity(item.entity) - self:PositionMonsterSlot(i) -end -self.TargetIndex = 1`), - method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then - return -end -monster:SetEnable(true) -monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]), - method('Shuffle', `if list == nil then -\treturn -end -for i = #list, 2, -1 do -\tlocal j = math.random(1, i) -\tlist[i], list[j] = list[j], list[i] -end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]), - method('BindButtons', `local endTurn = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton") -if endTurn ~= nil and endTurn.ButtonComponent ~= nil then - if self.EndTurnHandler ~= nil then - endTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler) - self.EndTurnHandler = nil - end - self.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end) -end -local drawPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DrawPile") -if drawPile ~= nil and drawPile.ButtonComponent ~= nil then - if self.DrawPileHandler ~= nil then - drawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler) - self.DrawPileHandler = nil - end - self.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("draw") end) -end -local discardPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/DiscardPile") -if discardPile ~= nil and discardPile.ButtonComponent ~= nil then - if self.DiscardPileHandler ~= nil then - discardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler) - self.DiscardPileHandler = nil - end - self.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("discard") end) -end -local exhaustPile = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/ExhaustPile") -if exhaustPile ~= nil and exhaustPile.ButtonComponent ~= nil then - if self.ExhaustPileHandler ~= nil then - exhaustPile:DisconnectEvent(ButtonClickEvent, self.ExhaustPileHandler) - self.ExhaustPileHandler = nil - end - self.ExhaustPileHandler = exhaustPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect("exhaust") end) -end -local inspectClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Close") -if inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then - if self.DeckInspectCloseHandler ~= nil then - inspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler) - self.DeckInspectCloseHandler = nil - end - self.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end) -end -local allDeckButton = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/AllDeckButton") -if allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then - if self.AllDeckHandler ~= nil then - allDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler) - self.AllDeckHandler = nil - end - self.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end) -end -local allDeckClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Close") -if allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then - if self.AllDeckCloseHandler ~= nil then - allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) - self.AllDeckCloseHandler = nil - end - self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) -end -self:BindClassDeckTabs() -for i = 1, 10 do - local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) - if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then - local cardPath = "/ui/DefaultGroup/CardHand/Card" .. tostring(i) - cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) - cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) - cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end) - cardEntity:ConnectEvent(UITouchDragEvent, function(ev) self:OnCardDrag(i, ev.TouchPoint) end) - cardEntity:ConnectEvent(UITouchEndDragEvent, function(ev) self:OnCardDragEnd(i, ev.TouchPoint) end) - cardEntity:ConnectEvent(UITouchEnterEvent, function() self:HoverCard(i) end) - cardEntity:ConnectEvent(UITouchExitEvent, function() self:UnhoverCard(i) end) - if cardEntity.ButtonComponent ~= nil then - cardEntity:ConnectEvent(ButtonClickEvent, function() self:OnCardButton(i) end) - end - end -end -for i = 1, 3 do - local rc = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Reward" .. tostring(i)) - if rc ~= nil and rc.ButtonComponent ~= nil then - rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end) - if rc.UITouchReceiveComponent ~= nil then - local cardPath = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(i) - rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) - rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) - end - end -end -local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip") -if skip ~= nil and skip.ButtonComponent ~= nil then - skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end) -end -local mapNodeIds = {} -for r = 1, ${MAP_ROWS} do - for c = 1, ${MAP_COLS} do - table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c)) - end -end -table.insert(mapNodeIds, "boss") -for i = 1, #mapNodeIds do - local nid = mapNodeIds[i] - local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid) - if mn ~= nil and mn.ButtonComponent ~= nil then - mn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end) - end -end -for i = 1, 3 do - local sc = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Card" .. tostring(i)) - if sc ~= nil and sc.ButtonComponent ~= nil then - sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end) - if sc.UITouchReceiveComponent ~= nil then - local cardPath = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i) - sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) - sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) - end - end -end -local shopLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Leave") -if shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then - shopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) -end -local shopRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic") -if shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then - shopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end) -end -local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave") -if restLeave ~= nil and restLeave.ButtonComponent ~= nil then - restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) -end -for i = 1, ${MAX_MONSTERS} do - local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)) - if ms ~= nil and ms.ButtonComponent ~= nil then - ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end) - end -end -for i = 1, 10 do - local rs = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)) - if rs ~= nil and rs.UITouchReceiveComponent ~= nil then - local idx = i - rs:ConnectEvent(UITouchEnterEvent, function() - local rid = nil - if self.RunRelics ~= nil then rid = self.RunRelics[idx] end - if rid ~= nil and self.Relics[rid] ~= nil then - self:ShowTooltip(self.Relics[rid].name, self.Relics[rid].desc, -240 + (idx - 1) * 48) - end - end) - rs:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end) - end -end -for i = 1, 5 do - local ps = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)) - if ps ~= nil and ps.UITouchReceiveComponent ~= nil then - local idx = i - ps:ConnectEvent(UITouchEnterEvent, function() - local pid = nil - if self.RunPotions ~= nil then pid = self.RunPotions[idx] end - if pid ~= nil and self.Potions[pid] ~= nil then - self:ShowTooltip(self.Potions[pid].name, self.Potions[pid].desc, 240 + (idx - 1) * 44) - end - end) - ps:ConnectEvent(UITouchExitEvent, function() self:HideTooltip() end) - ps:ConnectEvent(UITouchDownEvent, function() self:OpenPotionMenu(idx) end) - end -end -local pmUse = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Use") -if pmUse ~= nil and pmUse.ButtonComponent ~= nil then - pmUse:ConnectEvent(ButtonClickEvent, function() self:UsePotion() end) -end -local pmToss = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Toss") -if pmToss ~= nil and pmToss.ButtonComponent ~= nil then - pmToss:ConnectEvent(ButtonClickEvent, function() self:TossPotion() end) -end -local pmClose = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/PotionMenu/Close") -if pmClose ~= nil and pmClose.ButtonComponent ~= nil then - pmClose:ConnectEvent(ButtonClickEvent, function() self:ClosePotionMenu() end) -end -local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion") -if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then - shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end) -end -local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") -if chest ~= nil and chest.ButtonComponent ~= nil then - chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end) -end -local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave") -if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then - treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) -end -local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton") -if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then - jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end) -end -local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton") -if jcJob ~= nil and jcJob.ButtonComponent ~= nil then - jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end) -end -for i = 1, 3 do - local slotIdx = i - local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)) - if jb ~= nil and jb.ButtonComponent ~= nil then - jb:ConnectEvent(ButtonClickEvent, function() - if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then - self:SetJob(self.JobOpts[slotIdx].id) - end - end) - end -end`), - method('StartPlayerTurn', `self.Turn = self.Turn + 1 -self.Energy = self.MaxEnergy -self:ApplyRelics("turnStart") -self.PlayerBlock = 0 -if self.ClayBlockNext > 0 then - self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext - self.ClayBlockNext = 0 -end -if self.PlayerPowers ~= nil then - for i = 1, #self.PlayerPowers do - local pc = self.Cards[self.PlayerPowers[i]] - if pc ~= nil then - if pc.powerEffect == "strengthPerTurn" then - self.PlayerStr = self.PlayerStr + pc.value - elseif pc.powerEffect == "energyPerTurn" then - self.Energy = self.Energy + pc.value - elseif pc.powerEffect == "blockPerTurn" then - self.PlayerBlock = self.PlayerBlock + pc.value - end - if pc.turnStartShiv ~= nil then - self:AddCardsToHand("Shiv", pc.turnStartShiv) - end - end - end -end -self:DrawCards(5) -self:RenderHand(true) -self:RenderCombat()`), - method('EndPlayerTurn', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then - return -end -if self:IsDiscardSelecting() == true then - self:Toast("버릴 카드를 먼저 선택하세요") - return -end -local burn = 0 -for bi = 1, #self.Hand do -\tlocal hc = self.Cards[self.Hand[bi]] -\tif hc ~= nil and hc.endTurnDamage ~= nil then burn = burn + hc.endTurnDamage end -end -if burn > 0 then -\tself.PlayerHp = self.PlayerHp - burn -\tif self.PlayerHp < 0 then self.PlayerHp = 0 end -\tself:ShowPlayerDmgPop(burn) -\tself:RenderCombat() -end -local kept = {} -for i = 1, #self.Hand do -\tlocal cardId = self.Hand[i] -\tlocal c = self.Cards[cardId] -\tif c ~= nil and c.retain == true then -\t\ttable.insert(kept, cardId) -\telse -\t\ttable.insert(self.DiscardPile, cardId) -\tend -end -self.Hand = kept -if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end -if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end -self:RenderHand(false) -self:RenderPiles() -self:EnemyTurn()`), - method('DrawCards', `local drawnSlots = {} -for i = 1, amount do -\tif #self.DrawPile <= 0 then -\t\tself:RecycleDiscardIntoDraw() -\tend -\tif #self.DrawPile <= 0 then -\t\tbreak -\tend -\tlocal cardId = table.remove(self.DrawPile) -\tif #self.Hand >= 10 then -\t\ttable.insert(self.DiscardPile, cardId) -\t\tself:TriggerSly(cardId) -\telse -\t\ttable.insert(self.Hand, cardId) -\t\tif #self.Hand <= 5 then -\t\t\ttable.insert(drawnSlots, #self.Hand) -\t\tend -\tend -end -self:RenderPiles() -if animate == true and #drawnSlots > 0 then -\tself:RenderHand(false) -\tlocal drawStart = Vector2(-590, 8) -\tfor i = 1, #drawnSlots do -\t\tlocal slot = drawnSlots[i] -\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045) -\tend -end`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }, - ]), - method('AddCardsToHand', `if self.Hand == nil then - self.Hand = {} -end -if self.DiscardPile == nil then - self.DiscardPile = {} -end -for i = 1, amount do - if #self.Hand >= 10 then - table.insert(self.DiscardPile, cardId) - else - table.insert(self.Hand, cardId) - end -end -self:RenderHand(false) -self:RenderPiles()`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, - ]), - method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then -\treturn -end -self.DrawPile = {} -for i = 1, #self.DiscardPile do -\tself.DrawPile[i] = self.DiscardPile[i] -end -self.DiscardPile = {} -self:Shuffle(self.DrawPile)`), - method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", self:FormatNumber(#self.DrawPile)) -self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", self:FormatNumber(#self.DiscardPile)) -self:SetText("/ui/DefaultGroup/DeckHud/ExhaustPile/Count", self:FormatNumber(#(self.ExhaustPile or {}))) -self:SetText("/ui/DefaultGroup/DeckHud/EnergyOrb/Value", string.format("%d", self.Energy) .. "/" .. string.format("%d", self.MaxEnergy)) -local inspect = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") -if inspect ~= nil and inspect.Enable == true and self.DeckInspectKind ~= "" then - self:OpenDeckInspect(self.DeckInspectKind) -end`), - method('OpenDeckInspect', `self.DeckInspectKind = kind -if self.DeckAllOpen == true then - self.DeckAllOpen = false - local allHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") - if allHud ~= nil then - allHud.Enable = false - end -end -local pile = {} -local title = "" -if kind == "discard" then - pile = self.DiscardPile or {} - title = "버린 덱" -elseif kind == "exhaust" then - pile = self.ExhaustPile or {} - title = "소멸 덱" -else - pile = self.DrawPile or {} - title = "뽑을 덱" -end -self:RenderDeckInspect(pile, title) -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") -if hud ~= nil then - hud.Enable = true -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), - method('CloseDeckInspect', `self.DeckInspectKind = "" -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") -if hud ~= nil then - hud.Enable = false -end`), - method('RenderDeckInspect', `local count = 0 -if pile ~= nil then - count = #pile -end -local suffix = " (" .. tostring(count) .. ")" -if count > 60 then - suffix = suffix .. " - 60장까지 표시" -end -self:SetText("/ui/DefaultGroup/DeckInspectHud/Title", title .. suffix) -local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Empty") -if empty ~= nil then - empty.Enable = count <= 0 -end -for i = 1, 60 do - local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(i)) - if e ~= nil then - local cardId = nil - if pile ~= nil then - cardId = pile[i] - end - if cardId == nil then - e.Enable = false - else - e.Enable = true - self:ApplyInspectCardVisual(i, cardId) - end - end -end`, [ - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' }, - ]), - method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - ]), - method('BindClassDeckTabs', `local warriorTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/WarriorTab") -if warriorTab ~= nil and warriorTab.ButtonComponent ~= nil then - if self.WarriorDeckTabHandler ~= nil then - warriorTab:DisconnectEvent(ButtonClickEvent, self.WarriorDeckTabHandler) - self.WarriorDeckTabHandler = nil - end - self.WarriorDeckTabHandler = warriorTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("warrior") end) -end -local thiefTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/ThiefTab") -if thiefTab ~= nil and thiefTab.ButtonComponent ~= nil then - if self.ThiefDeckTabHandler ~= nil then - thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler) - self.ThiefDeckTabHandler = nil - end - self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end) -end -local mageTab = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/MageTab") -if mageTab ~= nil and mageTab.ButtonComponent ~= nil then - if self.MageDeckTabHandler ~= nil then - mageTab:DisconnectEvent(ButtonClickEvent, self.MageDeckTabHandler) - self.MageDeckTabHandler = nil - end - self.MageDeckTabHandler = mageTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("magician") end) -end`), - method('OpenClassDeck', `self.CodexMode = false -self.ClassDeckMode = true -self.DeckAllOpen = true -self:SetClassDeckTab(className) -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") -if hud ~= nil then - hud.Enable = true -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]), - method('SetClassDeckTab', `if self.ClassDeckMode ~= true then - return -end -self.ClassDeckCards = {} -self.ClassDeckTitle = "직업 덱" -if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then - className = "bandit" -end -self.ClassDeckClass = className -local allowed = {} -if className == "warrior" then - allowed["warrior"] = true - allowed["fighter"] = true - allowed["page"] = true - allowed["spearman"] = true - self.ClassDeckTitle = "전사 전체 덱" -elseif className == "magician" then - allowed["magician"] = true - allowed["firepoison"] = true - allowed["icelightning"] = true - allowed["cleric"] = true - self.ClassDeckTitle = "마법사 전체 덱" -else - allowed["bandit"] = true - allowed["shiv"] = true - allowed["poisoner"] = true - allowed["trickster"] = true - self.ClassDeckTitle = "도적 전체 덱" -end -for id, c in pairs(self.Cards) do - if c ~= nil and c.curse ~= true and allowed[c.class] == true then - table.insert(self.ClassDeckCards, id) - end -end -table.sort(self.ClassDeckCards, function(a, b) - local ca = self.Cards[a] - local cb = self.Cards[b] - local na = a - local nb = b - if ca ~= nil and ca.name ~= nil then na = ca.name end - if cb ~= nil and cb.name ~= nil then nb = cb.name end - if na == nb then return a < b end - return na < nb -end) -self:RenderAllDeck() -self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]), - method('RenderClassDeckTabs', `local tabs = { - { path = "/ui/DefaultGroup/DeckAllHud/WarriorTab", cls = "warrior" }, - { path = "/ui/DefaultGroup/DeckAllHud/ThiefTab", cls = "bandit" }, - { path = "/ui/DefaultGroup/DeckAllHud/MageTab", cls = "magician" }, -} -for i = 1, #tabs do - local e = _EntityService:GetEntityByPath(tabs[i].path) - if e ~= nil then - e.Enable = self.ClassDeckMode == true - if e.SpriteGUIRendererComponent ~= nil then - if self.ClassDeckClass == tabs[i].cls then - e.SpriteGUIRendererComponent.Color = Color(0.22, 0.28, 0.34, 1) - else - e.SpriteGUIRendererComponent.Color = Color(0.11, 0.13, 0.16, 1) - end - end - end -end`), - method('OpenAllDeck', `local inspectHud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckInspectHud") -if inspectHud ~= nil then - inspectHud.Enable = false -end -self.DeckInspectKind = "" -self.ClassDeckMode = false -self.ClassDeckClass = "" -self:RenderClassDeckTabs() -self.DeckAllOpen = true -self:RenderAllDeck() -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") -if hud ~= nil then - hud.Enable = true -end`), - method('CloseAllDeck', `self.DeckAllOpen = false -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud") -if hud ~= nil then - hud.Enable = false -end -if self.ClassDeckMode == true then - self.ClassDeckMode = false - self.ClassDeckCards = {} - self.ClassDeckTitle = "" - self.ClassDeckClass = "" -end -self:RenderClassDeckTabs() -if self.CodexMode == true then - self.CodexMode = false - self:ShowLobby() -end`), - method('RenderAllDeck', `local pile = self.RunDeck or {} -local title = "모든 덱" -if self.ClassDeckMode == true then - pile = self.ClassDeckCards or {} - title = self.ClassDeckTitle -elseif self.CodexMode == true then - pile = self.CodexCards or {} - title = "카드 도감" -end -local count = #pile -self:SetText("/ui/DefaultGroup/DeckAllHud/Title", title .. " (" .. tostring(count) .. ")") -self:RenderClassDeckTabs() -local empty = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Empty") -if empty ~= nil then - empty.Enable = count <= 0 -end -for i = 1, 120 do - local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(i)) - if e ~= nil then - local cardId = pile[i] - if cardId == nil then - e.Enable = false - else - e.Enable = true - self:ApplyAllDeckCardVisual(i, cardId) - end - end -end`), - method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - ]), - method('GetHandSlotX', `local n = 0 -if self.Hand ~= nil then - n = #self.Hand -end -if n <= 0 then - return 0 -end -local spacing = 175 -if n > 8 then spacing = math.floor(1400 / n) end -local startX = -((n - 1) * spacing) / 2 -return startX + (slot - 1) * spacing`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'), - method('RenderHand', `local n = #self.Hand -local spacing = 175 -if n > 8 then spacing = math.floor(1400 / n) end -local startX = -((n - 1) * spacing) / 2 -local drawStart = Vector2(-590, 8) -for i = 1, 10 do -\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) -\tif cardEntity ~= nil then -\t\tlocal cardId = self.Hand[i] -\t\tif cardId == nil then -\t\t\tcardEntity.Enable = false -\t\telse -\t\t\tcardEntity.Enable = true -\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.UIScale = Vector3(1, 1, 1) end -\t\t\tself:ApplyCardVisual(i, cardId) -\t\t\tlocal tx = self:GetHandSlotX(i) -\t\t\tif animate == true then -\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2(tx, 0), 0.16 + i * 0.03) -\t\t\telse -\t\t\t\tif cardEntity.UITransformComponent ~= nil then cardEntity.UITransformComponent.anchoredPosition = Vector2(tx, 0) end -\t\t\tend -\t\tend -\tend -end -self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]), - method('ApplyCardFace', `local c = self.Cards[cardId] -if c == nil then - c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" } -end -local e = _EntityService:GetEntityByPath(base) -if e ~= nil and e.SpriteGUIRendererComponent ~= nil then - if e.UITransformComponent ~= nil then - e.UITransformComponent.UIScale = Vector3(1, 1, 1) - end - local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"] - local ruid = nil - if frames ~= nil then - ruid = frames[c.rarity or "normal"] - end - if ruid ~= nil then - e.SpriteGUIRendererComponent.ImageRUID = ruid - e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) - end -end -self:SetText(base .. "/Cost", string.format("%d", c.cost)) -self:SetText(base .. "/Name", c.name) -self:SetText(base .. "/Desc", c.desc) -local art = _EntityService:GetEntityByPath(base .. "/Art") -if art ~= nil then - if c.image ~= nil and c.image ~= "" then - art.Enable = true - if art.SpriteGUIRendererComponent ~= nil then - art.SpriteGUIRendererComponent.ImageRUID = c.image - end - else - art.Enable = false - end -end`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - ]), - method('SetCardHover', `local prefix = "" -local count = 0 -local xs = {} -local baseY = 0 -local hoverIndex = 0 -local push = 110 -if string.find(path, "/ui/DefaultGroup/CardHand/Card") == 1 then - if self.DragSlot ~= nil and self.DragSlot > 0 then - return - end - prefix = "/ui/DefaultGroup/CardHand/Card" - count = 0 - if self.Hand ~= nil then count = #self.Hand end - for i = 1, count do - xs[i] = self:GetHandSlotX(i) - end - baseY = 0 - hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 -elseif string.find(path, "/ui/DefaultGroup/RewardHud/Reward") == 1 then - prefix = "/ui/DefaultGroup/RewardHud/Reward" - count = 3 - xs = { -300, 0, 300 } - baseY = 0 - hoverIndex = tonumber(string.match(path, "Reward(%d+)")) or 0 -elseif string.find(path, "/ui/DefaultGroup/ShopHud/Card") == 1 then - prefix = "/ui/DefaultGroup/ShopHud/Card" - count = 3 - xs = { -300, 0, 300 } - baseY = 20 - hoverIndex = tonumber(string.match(path, "Card(%d+)")) or 0 -end -if count <= 0 then - return -end -if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then - _TimerService:ClearTimer(self.CardHoverTweenId) - self.CardHoverTweenId = 0 -end -local items = {} -for i = 1, count do - local e = _EntityService:GetEntityByPath(prefix .. tostring(i)) - if e ~= nil and e.UITransformComponent ~= nil then - local tr = e.UITransformComponent - local tx = xs[i] - local ty = baseY - local sc = 1 - if hover == true and hoverIndex > 0 then - if i == hoverIndex and e.Enable == true then - sc = 1.5 - elseif i < hoverIndex then - tx = tx - push - elseif i > hoverIndex then - tx = tx + push - end - end - table.insert(items, { tr = tr, sx = tr.anchoredPosition.x, sy = tr.anchoredPosition.y, ss = tr.UIScale.x, tx = tx, ty = ty, ts = sc }) - end -end -local elapsed = 0 -local duration = 0.12 -local eventId = 0 -eventId = _TimerService:SetTimerRepeat(function() - elapsed = elapsed + 1 / 60 - local t = math.min(elapsed / duration, 1) - local eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) - for i = 1, #items do - local it = items[i] - local x = it.sx + (it.tx - it.sx) * eased - local y = it.sy + (it.ty - it.sy) * eased - local s = it.ss + (it.ts - it.ss) * eased - it.tr.anchoredPosition = Vector2(x, y) - it.tr.UIScale = Vector3(s, s, 1) - end - if t >= 1 then - _TimerService:ClearTimer(eventId) - if self.CardHoverTweenId == eventId then - self.CardHoverTweenId = 0 - end - end -end, 1 / 60) -self.CardHoverTweenId = eventId`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' }, - ]), - method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - ]), - method('SetText', `local entity = _EntityService:GetEntityByPath(path) -if entity ~= nil and entity.TextComponent ~= nil then -\tentity.TextComponent.Text = value -end`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }, - ]), - method('FormatNumber', `if value == nil then - return "" -end -local n = tonumber(value) -if n == nil then - return tostring(value) -end -if math.abs(n - math.floor(n)) < 0.00001 then - return string.format("%d", math.floor(n)) -end -return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'), - method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) -if cardEntity == nil or cardEntity.UITransformComponent == nil then -\treturn -end -local tr = cardEntity.UITransformComponent -tr.anchoredPosition = fromPos -local elapsed = 0 -local eventId = 0 -eventId = _TimerService:SetTimerRepeat(function() -\telapsed = elapsed + 1 / 60 -\tlocal t = math.min(elapsed / duration, 1) -\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) -\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased) -\tif t >= 1 then -\t\t_TimerService:ClearTimer(eventId) -\tend -end, 1 / 60)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' }, - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' }, - ]), - method('AddCardBlock', `local amount = base or 0 -if amount > 0 and self.PlayerDex ~= nil then - amount = amount + self.PlayerDex -end -if amount < 0 then - amount = 0 -end -self.PlayerBlock = self.PlayerBlock + amount -return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), - method('CalcPlayerAttack', `local base2 = base -self.FightAttackCount = self.FightAttackCount + 1 -if self.FightAttackCount == 1 and self:HasRelic("akabeko") then - base2 = base2 + 8 -end -local dmg = base2 + self.PlayerStr -if self:HasRelic("penNib") and self.FightAttackCount % 10 == 0 then - dmg = dmg * 2 -end -if self.PlayerWeak > 0 then - dmg = math.floor(dmg * 0.75) -end -if dmg > 0 and dmg < 5 and self:HasRelic("boot") then - dmg = 5 -end -if dmg < 0 then - dmg = 0 -end -return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), - method('ResolveCardEffects', `if c == nil then - return -end -if c.kind == "Attack" then - if c.damage ~= nil then - self:PlayerAttackMotion() - local total = 0 - local hitN = c.hits or 1 - for h = 1, hitN do - total = total + self:CalcPlayerAttack(c.damage) - end - if c.aoe == true then - self:PlayAoeFx(c.fx or c.image, total) - else - self:PlayAttackFx(self.TargetIndex, c.fx or c.image, total, c.pierce == true) - end - end - if c.block ~= nil then - self:AddCardBlock(c.block) - end - if free ~= true then - self:ApplyRelics("cardPlayed") - end -elseif c.kind == "Skill" then - if c.block ~= nil then - self:AddCardBlock(c.block) - end -elseif c.kind == "Power" then - if free ~= true then - table.insert(self.PlayerPowers, cardId) - end -end -if c.strength ~= nil then - self.PlayerStr = self.PlayerStr + c.strength -end -if c.dex ~= nil then - self.PlayerDex = self.PlayerDex + c.dex -end -if c.thorns ~= nil then - self.PlayerThorns = self.PlayerThorns + c.thorns -end -if c.selfVuln ~= nil then - self.PlayerVuln = self.PlayerVuln + c.selfVuln -end -if c.heal ~= nil then - self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp) -end -if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then - local tm = self.Monsters[self.TargetIndex] - if tm == nil or tm.alive ~= true then - for i = 1, #self.Monsters do - if self.Monsters[i].alive == true then tm = self.Monsters[i]; self.TargetIndex = i; break end - end - end - if tm ~= nil and tm.alive == true then - if c.weak ~= nil then tm.weak = tm.weak + c.weak end - if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end - if c.vuln ~= nil then - tm.vuln = tm.vuln + c.vuln - if self:HasRelic("championBelt") then - tm.weak = tm.weak + 1 - end - end - end -end -if c.draw ~= nil then - self:DrawCards(c.draw, true) -end -if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then - self:AddCardsToHand("Shiv", c.addShiv) -end`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'free' }, - ]), - method('TriggerSly', `local c = self.Cards[cardId] -if c == nil or c.sly ~= true then - return -end -self:Toast("교활 발동: " .. c.name) -self:ResolveCardEffects(cardId, c, true)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]), - method('DiscardHandCard', `if self.Hand == nil then - return -end -local cardId = self.Hand[slot] -if cardId == nil then - return -end -table.remove(self.Hand, slot) -table.insert(self.DiscardPile, cardId) -if triggerSly == true then - self:TriggerSly(cardId) -end`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'triggerSly' }, - ]), - method('IsDiscardSelecting', `return self.DiscardSelectRemaining ~= nil and self.DiscardSelectRemaining > 0`, [], 0, 'boolean'), - method('UpdateDiscardPrompt', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/DiscardPrompt") -if e == nil then - return -end -if self:IsDiscardSelecting() == true then - local picked = self.DiscardSelectTotal - self.DiscardSelectRemaining - self:SetText("/ui/DefaultGroup/CombatHud/DiscardPrompt", "버릴 카드 선택 " .. self:FormatNumber(picked + 1) .. "/" .. self:FormatNumber(self.DiscardSelectTotal)) - e.Enable = true -else - e.Enable = false -end`), - method('BeginDiscardSelection', `if c == nil or self.Hand == nil then - return false -end -local n = 0 -if c.discardAll == true then - n = #self.Hand -elseif c.discard ~= nil then - n = math.min(c.discard, #self.Hand) -end -if n <= 0 then - return false -end -self.DiscardSelectRemaining = n -self.DiscardSelectTotal = n -self.DiscardPostShiv = 0 -self.DiscardShivPerPick = 0 -if c.addShiv ~= nil then - self.DiscardPostShiv = c.addShiv -end -if c.addShivPerDiscard == true then - self.DiscardShivPerPick = 1 -end -self:UpdateDiscardPrompt() -self:Toast("버릴 카드를 선택하세요") -return true`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'boolean'), - method('FinishDiscardSelection', `self.DiscardSelectRemaining = 0 -self.DiscardSelectTotal = 0 -local shivCount = self.DiscardPostShiv or 0 -self.DiscardPostShiv = 0 -self.DiscardShivPerPick = 0 -self:UpdateDiscardPrompt() -if shivCount > 0 then - self:AddCardsToHand("Shiv", shivCount) -end -self:RenderHand(false) -self:RenderPiles() -self:RenderCombat() -self:CheckCombatEnd()`), - method('SelectDiscardSlot', `if self:IsDiscardSelecting() ~= true then - return false -end -if self.Hand == nil or self.Hand[slot] == nil then - return true -end -local discarded = self.Hand[slot] -self:DiscardHandCard(slot, true) -if discarded ~= nil and self.DiscardShivPerPick ~= nil and self.DiscardShivPerPick > 0 then - self.DiscardPostShiv = (self.DiscardPostShiv or 0) + self.DiscardShivPerPick -end -self.DiscardSelectRemaining = self.DiscardSelectRemaining - 1 -if self.DiscardSelectRemaining <= 0 or #self.Hand <= 0 then - self:FinishDiscardSelection() -else - self:UpdateDiscardPrompt() - self:RenderHand(false) - self:RenderPiles() - self:RenderCombat() -end -return true`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'boolean'), - method('PlayCard', `if self:IsDiscardSelecting() == true then - self:SelectDiscardSlot(slot) - return -end -if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then - return -end -if self.Hand == nil then - return -end -local cardId = self.Hand[slot] -if cardId == nil then - return -end -local c = self.Cards[cardId] -if c == nil then - return -end -if c.unplayable == true then - self:Toast("사용할 수 없는 카드입니다") - return -end -if self.Energy < c.cost then - self:Toast("에너지가 부족합니다") - return -end -self.Energy = self.Energy - c.cost -self:ResolveCardEffects(cardId, c, false) -table.remove(self.Hand, slot) -if c.exhaust == true then - if self.ExhaustPile == nil then self.ExhaustPile = {} end - table.insert(self.ExhaustPile, cardId) -elseif c.kind ~= "Power" then - table.insert(self.DiscardPile, cardId) -end -self:RenderHand(false) -self:RenderPiles() -self:RenderCombat() -if self:BeginDiscardSelection(c) == true then - return -end -self:RenderHand(false) -self:RenderPiles() -self:RenderCombat() -self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('OnCardButton', `if self:IsDiscardSelecting() == true then - self:SelectDiscardSlot(slot) -end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('FindMonsterAtTouch', `local best = 0 -local bestDist = 200 -for i = 1, #self.Monsters do - local m = self.Monsters[i] - if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then - local wp = m.entity.TransformComponent.WorldPosition - local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7)) - local dx = sp.x - touchPoint.x - local dy = sp.y - touchPoint.y - local d = math.sqrt(dx * dx + dy * dy) - if d < bestDist then - bestDist = d - best = i - end - end -end -return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'), - method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 -local shownTarget = self.TargetIndex -if dragActive == true then shownTarget = self.DragTargetIndex end -for i = 1, #self.Monsters do - local m = self.Monsters[i] - local active = false - if m ~= nil and m.alive == true and i == shownTarget then active = true end - self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active) - self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive) - self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive) -end`), - method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then - return -end -if self.Hand == nil or self.Hand[slot] == nil then - return -end -if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then - _TimerService:ClearTimer(self.CardHoverTweenId) - self.CardHoverTweenId = 0 -end -for i = 1, 10 do - local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) - if e ~= nil and e.UITransformComponent ~= nil then - e.UITransformComponent.UIScale = Vector3(1, 1, 1) - e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0) - end -end -self.DragSlot = slot -self.DragTargetIndex = 0 -self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('OnCardDrag', `if self.DragSlot ~= slot then - return -end -local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) -if e ~= nil and e.UITransformComponent ~= nil then - local ui = _UILogic:ScreenToUIPosition(touchPoint) - e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360) -end -local cardId = self.Hand[slot] -local c = nil -if cardId ~= nil then c = self.Cards[cardId] end -if c ~= nil and c.kind == "Attack" then - local best = self:FindMonsterAtTouch(touchPoint) - if best ~= self.DragTargetIndex then - self.DragTargetIndex = best - self:RenderTargetFrames() - end -end`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, - ]), - method('OnCardDragEnd', `if self.DragSlot ~= slot then - return -end -self.DragSlot = 0 -local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) -if e ~= nil and e.UITransformComponent ~= nil then - e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0) - e.UITransformComponent.UIScale = Vector3(1, 1, 1) -end -self:ResolveCardDrop(slot, touchPoint)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, - ]), - method('ResolveCardDrop', `if self:IsDiscardSelecting() == true then - self:SelectDiscardSlot(slot) - return -end -if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then - return -end -local cardId = self.Hand[slot] -if cardId == nil then - return -end -local c = self.Cards[cardId] -if c == nil then - return -end -if c.kind == "Attack" then - local best = self.DragTargetIndex or 0 - if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end - self.DragTargetIndex = 0 - if best > 0 then - self.TargetIndex = best - self:PlayCard(slot) - self:RenderTargetFrames() - else - self:RenderTargetFrames() - end -else - self.DragTargetIndex = 0 - self:RenderTargetFrames() - local ui = _UILogic:ScreenToUIPosition(touchPoint) - if ui.y > -180 then - self:PlayCard(slot) - end -end`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }, - ]), - method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]), - method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex] -if m == nil or m.alive ~= true then - m = nil - for i = 1, #self.Monsters do - if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end - end -end -if m == nil then - return -end -local dmg = amount -if m.vuln > 0 then - dmg = math.floor(dmg * 1.5) -end -if m.block > 0 and pierce ~= true then - local absorbed = math.min(m.block, dmg) - m.block = m.block - absorbed - dmg = dmg - absorbed -end -m.hp = m.hp - dmg -self:MonsterHitMotion(m.slot) -if m.hp <= 0 then - m.hp = 0 - self:KillMonster(m.slot) -end`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, - ]), - method('PlayAttackFx', `local m = self.Monsters[targetIndex] -if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then - self:DealDamageToTarget(damage, pierce) - self:RenderCombat() - self:CheckCombatEnd() - return -end -self.FxBusy = true -local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx") -if fx ~= nil then - if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then - fx.SpriteGUIRendererComponent.ImageRUID = image - end - if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then - local wp = m.entity.TransformComponent.WorldPosition - local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7)) - fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp) - end - fx.Enable = true -end -_TimerService:SetTimerOnce(function() - if fx ~= nil then fx.Enable = false end - self.FxBusy = false - local shown = damage - local mt = self.Monsters[targetIndex] - if mt ~= nil and mt.alive == true and mt.vuln > 0 then - shown = math.floor(damage * 1.5) - end - self:DealDamageToTarget(damage, pierce) - self:ShowDmgPop(targetIndex, shown) - self:RenderCombat() - self:CheckCombatEnd() -end, 0.35)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, - { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' }, - ]), - method('PlayAoeFx', `self.FxBusy = true -local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx") -if fx ~= nil then - if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then - fx.SpriteGUIRendererComponent.ImageRUID = image - end - if fx.UITransformComponent ~= nil then - fx.UITransformComponent.anchoredPosition = Vector2(300, 60) - end - fx.Enable = true -end -_TimerService:SetTimerOnce(function() - if fx ~= nil then fx.Enable = false end - self.FxBusy = false - for i = 1, #self.Monsters do - local m = self.Monsters[i] - if m ~= nil and m.alive == true then - local dmg = damage - if m.vuln > 0 then - dmg = math.floor(dmg * 1.5) - end - if m.block > 0 then - local absorbed = math.min(m.block, dmg) - m.block = m.block - absorbed - dmg = dmg - absorbed - end - m.hp = m.hp - dmg - self:ShowDmgPop(i, dmg) - self:MonsterHitMotion(i) - if m.hp <= 0 then - m.hp = 0 - self:KillMonster(m.slot) - end - end - end - self:RenderCombat() - self:CheckCombatEnd() -end, 0.35)`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, - ]), - method('KillMonster', `local m = self.Monsters[slot] -if m == nil then - return -end -m.alive = false -if m.entity ~= nil and isvalid(m.entity) then - local ent = m.entity - _TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4) -end -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false) -for i = 1, #self.Monsters do - if self.Monsters[i].alive == true then self.TargetIndex = i; break end -end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('DealDamageToPlayer', `local dmg = amount -if self.PlayerBlock > 0 then - local absorbed = math.min(self.PlayerBlock, dmg) - self.PlayerBlock = self.PlayerBlock - absorbed - dmg = dmg - absorbed -end -if dmg > 0 then - self.PlayerHp = self.PlayerHp - dmg - local reflect = self.PlayerThorns or 0 - if self:HasRelic("bronzeScales") then - reflect = reflect + 3 - end - if reflect > 0 and attackerSlot ~= nil and attackerSlot > 0 then - local am = self.Monsters[attackerSlot] - if am ~= nil and am.alive == true then - am.hp = am.hp - reflect - self:ShowDmgPop(am.slot, reflect) - self:MonsterHitMotion(am.slot) - if am.hp <= 0 then - am.hp = 0 - self:KillMonster(am.slot) - end - end - end - if self:HasRelic("selfFormingClay") then - self.ClayBlockNext = self.ClayBlockNext + 3 - end - if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then - self.FirstHpLossDone = true - self:DrawCards(3) - self:RenderHand(false) - end -end -if self.PlayerHp < 0 then - self.PlayerHp = 0 -end`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' }, - ]), - method('EnemyTurn', `self.TurnBusy = true -self:EnemyActStep(1)`), - method('EnemyActStep', `local idx = 0 -for i = fromIndex, #self.Monsters do - if self.Monsters[i].alive == true then idx = i; break end -end -if idx == 0 or self.PlayerHp <= 0 then - self:FinishEnemyTurn() - return -end -local m = self.Monsters[idx] -local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx) -self:SetEntityEnabled(base .. "/ActFrame", true) -_TimerService:SetTimerOnce(function() - if m.poison ~= nil and m.poison > 0 then - m.hp = m.hp - m.poison - self:ShowDmgPop(idx, m.poison) - self:MonsterHitMotion(idx) - m.poison = m.poison - 1 - if m.hp <= 0 then - m.hp = 0 - self:KillMonster(m.slot) - self:RenderCombat() - self:SetEntityEnabled(base .. "/ActFrame", false) - _TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15) - return - end - end - m.block = 0 - local intent = m.intents[m.intentIdx] - if intent ~= nil then - if intent.kind == "Attack" then - self:MonsterLunge(idx) - 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 - local before = self.PlayerHp - self:DealDamageToPlayer(atk, idx) - self:ShowPlayerDmgPop(before - self.PlayerHp) - self:PlayerHitMotion() - elseif intent.kind == "Defend" then - m.block = m.block + intent.value - elseif intent.kind == "Debuff" then - if intent.effect == "weak" then - self.PlayerWeak = self.PlayerWeak + intent.value - elseif intent.effect == "vuln" then - self.PlayerVuln = self.PlayerVuln + intent.value - end - elseif intent.kind == "AddCard" then - local cnt = intent.count or 1 - for ci = 1, cnt do - table.insert(self.DiscardPile, intent.card) - end - self:RenderPiles() - local cn = intent.card - local cc = self.Cards[intent.card] - if cc ~= nil then cn = cc.name end - self:Toast(m.name .. ": " .. cn .. " 추가!") - end - end - if #m.intents > 0 then - m.intentIdx = math.random(1, #m.intents) - end - if m.weak > 0 then m.weak = m.weak - 1 end - if m.vuln > 0 then m.vuln = m.vuln - 1 end - self:RenderCombat() - self:SetEntityEnabled(base .. "/ActFrame", false) - _TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15) -end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]), - method('FinishEnemyTurn', `self.TurnBusy = false -self:CheckCombatEnd() -if self.CombatOver == true then - return -end -_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`), - method('ClearCombatCards', `self.DrawPile = {} -self.DiscardPile = {} -self.ExhaustPile = {} -self.Hand = {} -self.DiscardSelectRemaining = 0 -self.DiscardSelectTotal = 0 -self.DiscardPostShiv = 0 -self.DiscardShivPerPick = 0 -self:UpdateDiscardPrompt() -self:RenderHand(false) -self:RenderPiles()`), - method('CheckCombatEnd', `local anyAlive = false -for i = 1, #self.Monsters do - if self.Monsters[i].alive == true then anyAlive = true; break end -end -if anyAlive == false then - self.CombatOver = true - self:ClearCombatCards() - self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult()) - self:ApplyRelics("combatEnd") - self:ApplyRelics("combatReward") - self:MaybeDropPotion() - self:RenderRun() - local node = self.MapNodes[self.CurrentNodeId] - if node ~= nil and node.type == "elite" then - self.Gold = self.Gold + 15 - local nid = self:PickNewRelic() - if nid ~= "" then - self:AddRelic(nid) - local nr = self.Relics[nid] - if nr ~= nil then - self:Toast("유물 획득: " .. nr.name) - end - end - end - if node ~= nil and node.type == "boss" then - if self.PlayerJob == "" and self.Floor < self.RunLength then - self:ShowJobChoice() - else - if self.PlayerJob ~= "" then self:AwardSouls(1) end - local bid = self:PickNewRelic() - if bid ~= "" then - self:AddRelic(bid) - local br = self.Relics[bid] - if br ~= nil then - self:Toast("유물 획득: " .. br.name) - end - end - self:ContinueAfterBoss() - end - else - self:OfferReward() - end -elseif self.PlayerHp <= 0 then - self.CombatOver = true - self:EndRun("패배...") -end`), - method('ContinueAfterBoss', `if self.Floor < self.RunLength then - self.Floor = self.Floor + 1 - self.CurrentNodeId = "" - self.CurrentEnemyId = "" - self:GenerateMap() - self:RenderRun() - self:TeleportToActMap() - self:ShowMap() -else - self:EndRun("런 클리어!") -end`), - method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) -self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) -self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`), - method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) -if kind == "relic" then - local bid = self:PickNewRelic() - if bid ~= "" then - self:AddRelic(bid) - local br = self.Relics[bid] - if br ~= nil then - self:Toast("유물 획득: " .. br.name) - end - end - self:ContinueAfterBoss() -else - self:ShowJobSelect() -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), - method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass] -if opts == nil then - opts = self.Jobs["warrior"] -end -self.JobOpts = opts -for i = 1, 3 do - local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i) - local o = opts[i] - if o ~= nil then - self:SetEntityEnabled(base, true) - self:SetText(base .. "/Name", o.name) - self:SetText(base .. "/Desc", o.desc) - local sc = self.Cards[o.starter] - if sc ~= nil then - self:SetText(base .. "/Starter", "대표 카드: " .. sc.name) - end - else - self:SetEntityEnabled(base, false) - end -end -self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`), - method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then - for cls, list in pairs(self.Jobs) do - for i = 1, #list do - if list[i].id == self.PlayerJob then - return list[i].name - end - end - end -end -if self.SelectedClass == "warrior" then - return "전사" -elseif self.SelectedClass == "bandit" then - return "도적" -elseif self.SelectedClass == "magician" then - return "마법사" -end -return "플레이어"`, [], 0, 'string'), - method('SetJob', `self.PlayerJob = jobId -local starter = "" -local opts = self.Jobs[self.SelectedClass] or {} -for i = 1, #opts do - if opts[i].id == jobId then - starter = opts[i].starter - end -end -if starter ~= "" then - table.insert(self.RunDeck, starter) - local sc = self.Cards[starter] - if sc ~= nil then - self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name) - end -end -self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) -self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) -self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]), - 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/DefaultGroup/CombatHud/Result", text) -local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/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' }]), - 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/DefaultGroup/CombatHud/MonsterSlot" .. 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 .. "/TargetFrame", i == shownTarget) - 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/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp)) -self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220) -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) -self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) -local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) -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/DefaultGroup/CombatHud/PlayerPanel/Buffs", pb) -self:RenderRun()`), - method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0)) -local base = "/ui/DefaultGroup/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/DefaultGroup/CombatHud/MonsterSlot" .. 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/DefaultGroup/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/DefaultGroup/CombatHud/MonsterSlot" .. 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/DefaultGroup/CombatHud/TopBar/Floor", floorText) -self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`), - method('CardPool', `local pool = {} -for id, c in pairs(self.Cards) do - if c.token ~= true and (c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob)) then - table.insert(pool, id) - end -end -table.sort(pool) -return pool`, [], 0, 'any'), - method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) -self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) -local pool = self:CardPool() -local byRarity = {} -for _, id in ipairs(pool) do - local r = self.Cards[id].rarity or "normal" - if byRarity[r] == nil then byRarity[r] = {} end - table.insert(byRarity[r], id) -end -self.RewardChoices = {} -for i = 1, 3 do - local roll = math.random(1, 100) - local want = "normal" - if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end - local bucket = byRarity[want] - if bucket == nil or #bucket == 0 then bucket = pool end - self.RewardChoices[i] = bucket[math.random(1, #bucket)] - self:ApplyRewardVisual(i, self.RewardChoices[i]) -end -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud") -if hud ~= nil then - hud.Enable = true -end`), - method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [ - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, - ]), - method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then - return -end -if slot ~= 0 and self.RewardChoices ~= nil then - local id = self.RewardChoices[slot] - if id ~= nil then - table.insert(self.RunDeck, id) - end -end -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud") -if hud ~= nil then - hud.Enable = false -end -self:ShowMap()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('HasRelic', `if self.RunRelics == nil then - return false -end -for i = 1, #self.RunRelics do - if self.RunRelics[i] == id then - return true - end -end -return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'), - method('ApplyRelics', `if self.RunRelics == nil then - return -end -for i = 1, #self.RunRelics do - local r = self.Relics[self.RunRelics[i]] - if r ~= nil and r.hook == hook then - if r.effect == "block" then - self.PlayerBlock = self.PlayerBlock + r.value - elseif r.effect == "energy" then - self.Energy = self.Energy + r.value - elseif r.effect == "strength" then - self.PlayerStr = self.PlayerStr + r.value - elseif r.effect == "draw" then - self:DrawCards(r.value) - self:RenderHand(false) - elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then - self.PlayerHp = self.PlayerHp + r.value - if self.PlayerHp > self.PlayerMaxHp then - self.PlayerHp = self.PlayerMaxHp - end - elseif r.effect == "healIfLow" then - if self.PlayerHp * 2 <= self.PlayerMaxHp then - self.PlayerHp = self.PlayerHp + r.value - if self.PlayerHp > self.PlayerMaxHp then - self.PlayerHp = self.PlayerMaxHp - end - end - elseif r.effect == "gold" then - self.Gold = self.Gold + r.value - end - end -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hook' }]), - method('AddRelic', `if self.RunRelics == nil then - self.RunRelics = {} -end -table.insert(self.RunRelics, id) -local r = self.Relics[id] -if r ~= nil and r.hook == "passive" then - if r.effect == "potionSlots" then - self.PotionSlots = r.value - self:RenderPotions() - elseif r.effect == "maxHp" then - self.PlayerMaxHp = self.PlayerMaxHp + r.value - self.PlayerHp = self.PlayerHp + r.value - end -end -self:RenderRelics()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), - method('PickNewRelic', `local pool = {} -for i = 1, #self.RelicPool do - if self:HasRelic(self.RelicPool[i]) == false then - table.insert(pool, self.RelicPool[i]) - end -end -if #pool == 0 then - self.Gold = self.Gold + 25 - self:Toast("유물을 모두 모았습니다! 메소 +25") - return "" -end -return pool[math.random(1, #pool)]`, [], 0, 'string'), - method('AddPotion', `if self.RunPotions == nil then - self.RunPotions = {} -end -if #self.RunPotions >= self.PotionSlots then - self:Toast("물약 슬롯이 가득 찼습니다") - return false -end -table.insert(self.RunPotions, pid) -self:RenderPotions() -return true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pid' }], 0, 'boolean'), - method('MaybeDropPotion', `if math.random() > ${POTIONS.dropChance} then - return -end -local keys = {} -for pid, _ in pairs(self.Potions) do - table.insert(keys, pid) -end -table.sort(keys) -local pid = keys[math.random(1, #keys)] -if self:AddPotion(pid) == true then - local p = self.Potions[pid] - self:Toast("물약 획득: " .. p.name) -end`), - method('RenderPotions', `for i = 1, 5 do - local base = "/ui/DefaultGroup/CombatHud/TopBar/PotionSlot" .. tostring(i) - local e = _EntityService:GetEntityByPath(base) - if e ~= nil and e.SpriteGUIRendererComponent ~= nil then - local pid = nil - if self.RunPotions ~= nil then - pid = self.RunPotions[i] - end - if pid ~= nil and self.Potions[pid] ~= nil then - e.SpriteGUIRendererComponent.ImageRUID = self.Potions[pid].icon - e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) - elseif i > self.PotionSlots then - e.SpriteGUIRendererComponent.ImageRUID = "" - e.SpriteGUIRendererComponent.Color = Color(0.1, 0.1, 0.12, 0.85) - else - e.SpriteGUIRendererComponent.ImageRUID = "" - e.SpriteGUIRendererComponent.Color = Color(0.22, 0.25, 0.3, 0.9) - end - end -end`), - method('OpenPotionMenu', `if self.RunPotions == nil or self.RunPotions[slot] == nil then - return -end -self.PotionMenuSlot = slot -local pid = self.RunPotions[slot] -local p = self.Potions[pid] -if p ~= nil then - self:SetText("/ui/DefaultGroup/CombatHud/PotionMenu/Title", p.name .. " — " .. p.desc) -end -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", true)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('ClosePotionMenu', `self.PotionMenuSlot = 0 -self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false)`), - method('UsePotion', `if self.PotionMenuSlot <= 0 then - return -end -if self.CombatOver == true or self.TurnBusy == true or self.FxBusy == true then - self:Toast("지금은 사용할 수 없습니다") - return -end -local combat = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud") -local hand = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand") -if combat == nil or combat.Enable ~= true or hand == nil or hand.Enable ~= true then - self:Toast("전투 중에만 사용할 수 있습니다") - return -end -local pid = self.RunPotions[self.PotionMenuSlot] -if pid == nil then - return -end -local p = self.Potions[pid] -if p == nil then - return -end -if p.effect == "heal" then - self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp) -elseif p.effect == "damage" then - self:DealDamageToTarget(p.value, false) - self:ShowDmgPop(self.TargetIndex, p.value) -elseif p.effect == "strength" then - self.PlayerStr = self.PlayerStr + p.value -elseif p.effect == "block" then - self.PlayerBlock = self.PlayerBlock + p.value -elseif p.effect == "energy" then - self.Energy = self.Energy + p.value -elseif p.effect == "weak" then - local tm = self.Monsters[self.TargetIndex] - if tm ~= nil and tm.alive == true then - tm.weak = tm.weak + p.value - end -end -table.remove(self.RunPotions, self.PotionMenuSlot) -self:Toast("물약 사용: " .. p.name) -self:ClosePotionMenu() -self:RenderPotions() -self:RenderPiles() -self:RenderCombat() -self:CheckCombatEnd()`), - method('TossPotion', `if self.PotionMenuSlot <= 0 then - return -end -local pid = self.RunPotions[self.PotionMenuSlot] -if pid ~= nil then - local p = self.Potions[pid] - table.remove(self.RunPotions, self.PotionMenuSlot) - if p ~= nil then - self:Toast("물약 버림: " .. p.name) - end -end -self:ClosePotionMenu() -self:RenderPotions()`), - method('RenderRelics', `local count = 0 -if self.RunRelics ~= nil then - count = #self.RunRelics -end -for i = 1, 10 do - local base = "/ui/DefaultGroup/CombatHud/TopBar/RelicSlot" .. tostring(i) - local e = _EntityService:GetEntityByPath(base) - if e ~= nil and e.SpriteGUIRendererComponent ~= nil then - local rid = nil - if self.RunRelics ~= nil then - rid = self.RunRelics[i] - end - if rid ~= nil and self.Relics[rid] ~= nil and (i < 10 or count <= 10) then - e.SpriteGUIRendererComponent.ImageRUID = self.Relics[rid].icon - e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) - else - e.SpriteGUIRendererComponent.ImageRUID = "" - e.SpriteGUIRendererComponent.Color = Color(0.15, 0.16, 0.2, 0.6) - end - end -end -local of = "" -if count > 10 then - of = "+" .. tostring(count - 9) -end -self:SetText("/ui/DefaultGroup/CombatHud/TopBar/RelicOverflow", of)`), - method('BuildCardKeywordTooltip', `if c == nil then - return "" -end -local lines = {} -local function add(name, desc) - for i = 1, #lines do - if string.find(lines[i], name .. ":", 1, true) == 1 then - return - end - end - table.insert(lines, name .. ": " .. desc) -end -local cardDesc = c.desc or "" -if c.sly == true or string.find(cardDesc, "교활", 1, true) ~= nil then - add("교활", "버려지면 비용 없이 사용됩니다.") -end -if c.retain == true or string.find(cardDesc, "보존", 1, true) ~= nil then - add("보존", "턴 종료 시 버려지지 않고 손에 남습니다.") -end -if c.dex ~= nil and c.dex > 0 or string.find(cardDesc, "민첩", 1, true) ~= nil then - add("민첩", "카드로 얻는 방어도가 증가합니다.") -end -if c.thorns ~= nil and c.thorns > 0 or string.find(cardDesc, "가시", 1, true) ~= nil then - add("가시", "피해를 받으면 공격자에게 반사 피해를 줍니다.") -end -if c.exhaust == true or string.find(cardDesc, "소멸.", 1, true) ~= nil then - add("소멸", "사용 후 소멸 덱으로 이동해 이번 전투 동안 다시 나오지 않습니다.") -end -if string.find(cardDesc, "선천성", 1, true) ~= nil then - add("선천성", "전투 시작 시 손패에 들어옵니다.") -end -if c.vuln ~= nil and c.vuln > 0 then - add("취약", "받는 공격 피해가 50% 증가합니다.") -end -if c.weak ~= nil and c.weak > 0 then - add("약화", "주는 공격 피해가 25% 감소합니다.") -end -if c.poison ~= nil and c.poison > 0 then - add("중독", "턴 시작 시 체력을 잃고 수치가 1 감소합니다.") -end -if c.pierce == true then - add("관통", "방어도를 무시하고 피해를 줍니다.") -end -if c.aoe == true then - add("전체", "모든 적에게 적용됩니다.") -end -if c.kind == "Power" then - add("파워", "사용하면 전투 동안 지속 효과로 남습니다.") -end -if c.unplayable == true then - add("저주", "사용할 수 없고 손패를 방해합니다.") -end -local out = "" -for i = 1, #lines do - if i > 1 then out = out .. "\\n" end - out = out .. lines[i] -end -return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'), - method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then - return -end -local cardId = self.Hand[slot] -if cardId == nil then - return -end -local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) -local tx = 0 -if e ~= nil and e.UITransformComponent ~= nil then - tx = e.UITransformComponent.anchoredPosition.x - e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1) -end -local c = self.Cards[cardId] -if c ~= nil then - local tip = self:BuildCardKeywordTooltip(c) - if tip ~= "" then - local tipX = tx + 270 - if tx > 180 then tipX = tx - 270 end - if tipX > 760 then tipX = tx - 270 end - if tipX < -760 then tipX = tx + 270 end - self:ShowTooltipAt("키워드", tip, tipX, 90) - else - self:HideTooltip() - end -end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) -if e ~= nil and e.UITransformComponent ~= nil then - e.UITransformComponent.UIScale = Vector3(1, 1, 1) -end -self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('ShowTooltip', `self:ShowTooltipAt(name, desc, x, 400)`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, - ]), - method('ShowTooltipAt', `self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Name", name) -self:SetText("/ui/DefaultGroup/CombatHud/TooltipBox/Desc", desc) -local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/TooltipBox") -if e ~= nil then - if e.UITransformComponent ~= nil then - e.UITransformComponent.anchoredPosition = Vector2(x, y) - end - e.Enable = true -end`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'name' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, - { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' }, - ]), - method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`), - method('ShowMap', `self:ShowState("map") -self:RenderMap()`), - method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지 -self.MapNodes = {} -self.MapStart = {} -self.VisitedNodes = {} -self.Depth = 0 -self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} } -local cols = { 1, 2, 3, 4 } -for i = #cols, 2, -1 do - local j = math.random(1, i) - cols[i], cols[j] = cols[j], cols[i] -end -local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) } -for p = 1, 4 do - local c = starts[p] - local sid = "r1c" .. tostring(c) - if self.MapNodes[sid] == nil then - self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} } - end - local found = false - for i = 1, #self.MapStart do - if self.MapStart[i] == sid then found = true end - end - if found == false then - table.insert(self.MapStart, sid) - end - for r = 1, ${MAP_ROWS} - 1 do - local nc = c + math.random(-1, 1) - if nc < 1 then nc = 1 end - if nc > ${MAP_COLS} then nc = ${MAP_COLS} end - local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc) - if self.MapNodes[nid] == nil then - self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} } - end - local fid = "r" .. tostring(r) .. "c" .. tostring(c) - local dup = false - for i = 1, #self.MapNodes[fid].next do - if self.MapNodes[fid].next[i] == nid then dup = true end - end - if dup == false then - table.insert(self.MapNodes[fid].next, nid) - end - c = nc - end - local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) - local bdup = false - for i = 1, #self.MapNodes[lid].next do - if self.MapNodes[lid].next[i] == "boss" then bdup = true end - end - if bdup == false then - table.insert(self.MapNodes[lid].next, "boss") - end -end -for r = 3, ${MAP_ROWS} do - for c = 1, ${MAP_COLS} do - local id = "r" .. tostring(r) .. "c" .. tostring(c) - local node = self.MapNodes[id] - if node ~= nil then - -- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지) - local parentTypes = {} - for pid, pn in pairs(self.MapNodes) do - if pn.row == r - 1 then - for i = 1, #pn.next do - if pn.next[i] == id then parentTypes[pn.type] = true end - end - end - end - local w - if r == ${MAP_ROWS} then - w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } } - elseif r >= 4 then - w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } } - else - w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } } - end - local total = 0 - for i = 1, #w do - local t = w[i][1] - if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == true then - w[i][2] = 0 - end - total = total + w[i][2] - end - local roll = math.random() * total - local acc = 0 - for i = 1, #w do - acc = acc + w[i][2] - if roll <= acc then - node.type = w[i][1] - break - end - end - end - end -end`), - method('IsReachable', `local list -if self.CurrentNodeId == "" then - list = self.MapStart -else - local node = self.MapNodes[self.CurrentNodeId] - if node == nil then - return false - end - list = node.next -end -for i = 1, #list do - if list[i] == id then - return true - end -end -return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'), - method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id -local e = _EntityService:GetEntityByPath(base) -if e == nil then - return -end -local node = self.MapNodes[id] -if node == nil then - e.Enable = false - return -end -e.Enable = true -local ruid = self.NodeIcons[node.type] -if ruid == nil then - ruid = self.NodeIcons["combat"] -end -if e.SpriteGUIRendererComponent ~= nil and ruid ~= nil then - e.SpriteGUIRendererComponent.ImageRUID = ruid -end -local reachable = self:IsReachable(id) -local visited = false -if self.VisitedNodes ~= nil then - for i = 1, #self.VisitedNodes do - if self.VisitedNodes[i] == id then visited = true end - end -end -if e.SpriteGUIRendererComponent ~= nil then - if id == self.CurrentNodeId then - e.SpriteGUIRendererComponent.Color = Color(1, 0.82, 0.3, 1) - elseif visited == true then - e.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.9) - elseif reachable == true then - e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) - else - e.SpriteGUIRendererComponent.Color = Color(0.68, 0.68, 0.72, 0.85) - end -end -if e.ButtonComponent ~= nil then - e.ButtonComponent.Enable = reachable -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), - method('RenderMapDots', `local node = self.MapNodes[fromId] -local has = false -if node ~= nil then - for i = 1, #node.next do - if node.next[i] == toId then has = true end - end -end -for k = 1, 3 do - local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k)) - if d ~= nil then - d.Enable = has - if has == true and d.SpriteGUIRendererComponent ~= nil then - if fromId == self.CurrentNodeId then - d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1) - else - d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8) - end - end - end -end`, [ - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' }, - { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' }, - ]), - method('RenderMap', `for r = 1, ${MAP_ROWS} do - for c = 1, ${MAP_COLS} do - self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c)) - end -end -self:RenderMapNode("boss") -for r = 1, ${MAP_ROWS} - 1 do - for c = 1, ${MAP_COLS} do - local fid = "r" .. tostring(r) .. "c" .. tostring(c) - for c2 = c - 1, c + 1 do - if c2 >= 1 and c2 <= ${MAP_COLS} then - self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2)) - end - end - end -end -for c = 1, ${MAP_COLS} do - local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) - self:RenderMapDots(fid .. "_b", fid, "boss") -end -`), - method('PickNode', `if self.RunActive ~= true then - return -end -if self:IsReachable(id) ~= true then - return -end -self.CurrentNodeId = id -if self.VisitedNodes == nil then - self.VisitedNodes = {} -end -table.insert(self.VisitedNodes, id) -local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud") -if hud ~= nil then - hud.Enable = false -end -local node = self.MapNodes[id] -self.Depth = node.row -self:RenderRun() -if node.type == "shop" then - self:ShowShop() -elseif node.type == "rest" then - self:ShowRest() -elseif node.type == "treasure" then - self:ShowTreasure() -else - self.CurrentEnemyId = "" - self:StartCombat() -end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), - method('ShowShop', `local pool = self:CardPool() -self.ShopChoices = {} -self.ShopBought = { false, false, false } -for i = 1, 3 do - self.ShopChoices[i] = pool[math.random(1, #pool)] -end -self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)] -self.ShopRelicBought = false -local pkeys = {} -for pid, _ in pairs(self.Potions) do - table.insert(pkeys, pid) -end -table.sort(pkeys) -self.ShopPotion = pkeys[math.random(1, #pkeys)] -self.ShopPotionBought = false -self:RenderShop() -self:ShowState("shop")`), - method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold)) -for i = 1, 3 do - local cid = self.ShopChoices[i] - local c = self.Cards[cid] - local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i) - if c ~= nil then - self:ApplyCardFace(base, cid) - self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소") - local e = _EntityService:GetEntityByPath(base) - if e ~= nil and e.SpriteGUIRendererComponent ~= nil then - if self.ShopBought[i] == true then - e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) - end - end - end -end -local rr = self.Relics[self.ShopRelic] -if rr ~= nil then - self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc) - self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소") - local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic") - if re ~= nil and re.SpriteGUIRendererComponent ~= nil then - if self.ShopRelicBought == true then - re.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) - else - re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1) - end - end -end -local pp = self.Potions[self.ShopPotion] -if pp ~= nil then - self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc) - self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소") - local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion") - if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then - if self.ShopPotionBought == true then - pe.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6) - else - pe.SpriteGUIRendererComponent.Color = Color(0.45, 0.7, 0.55, 1) - end - end -end`), - method('BuyRelic', `if self.ShopRelicBought == true then - return -end -if self.Gold < ${RELIC_PRICE} then - return -end -self.Gold = self.Gold - ${RELIC_PRICE} -self:AddRelic(self.ShopRelic) -self.ShopRelicBought = true -self:RenderShop() -self:RenderRun()`), - method('BuyPotion', `if self.ShopPotionBought == true then - return -end -if self.Gold < ${POTIONS.shopPrice} then - return -end -if self.RunPotions ~= nil and #self.RunPotions >= self.PotionSlots then - self:Toast("물약 슬롯이 가득 찼습니다") - return -end -if self:AddPotion(self.ShopPotion) == true then - self.Gold = self.Gold - ${POTIONS.shopPrice} - self.ShopPotionBought = true -end -self:RenderShop() -self:RenderRun()`), - method('BuyCard', `if self.ShopBought == nil or self.ShopBought[slot] == true then - return -end -if self.Gold < ${CARD_PRICE} then - return -end -self.Gold = self.Gold - ${CARD_PRICE} -table.insert(self.RunDeck, self.ShopChoices[slot]) -self.ShopBought[slot] = true -self:RenderShop() -self:RenderRun()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), - method('ShowRest', `local old = self.PlayerHp -self.PlayerHp = self.PlayerHp + ${REST_HEAL} -if self.PlayerHp > self.PlayerMaxHp then - self.PlayerHp = self.PlayerMaxHp -end -local healed = self.PlayerHp - old -self:SetText("/ui/DefaultGroup/RestHud/Info", "HP " .. string.format("%d", old) .. " → " .. string.format("%d", self.PlayerHp) .. " (+" .. string.format("%d", healed) .. ")") -self:RenderCombat() -self:ShowState("rest")`), - method('LeaveNode', `local s = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud") -if s ~= nil then - s.Enable = false -end -local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud") -if r ~= nil then - r.Enable = false -end -local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud") -if t ~= nil then - t.Enable = false -end -self:ShowMap()`), - method('ShowTreasure', `self.ChestOpened = false -local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") -if chest ~= nil then - if chest.SpriteGUIRendererComponent ~= nil then - chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}" - end - if chest.UITransformComponent ~= nil then - chest.UITransformComponent.anchoredPosition = Vector2(0, 40) - end -end -self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false) -self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true) -self:ShowState("treasure")`), - method('OpenChest', `if self.ChestOpened == true then - return -end -self.ChestOpened = true -self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false) -local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest") -local steps = { 10, -10, 8, -8, 5, 0 } -for i = 1, #steps do - local dx = steps[i] - _TimerService:SetTimerOnce(function() - if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then - chest.UITransformComponent.anchoredPosition = Vector2(dx, 40) - end - end, 0.08 * i) -end -_TimerService:SetTimerOnce(function() - if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then - chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}" - end - local g = 40 + math.random(0, 20) - local nid = self:PickNewRelic() - local msg = "" - if nid ~= "" then - self:AddRelic(nid) - local nr = self.Relics[nid] - msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g) - else - g = g + 30 - msg = "메소 +" .. tostring(g) - end - self.Gold = self.Gold + g - self:RenderRun() - self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg) - self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true) -end, 0.55)`), + ...bootMethods, + ...stateMethods, + ...soulMethods, + ...charSelectMethods, + ...runMethods, + ...deckTurnMethods, + ...deckViewMethods, + ...handMethods, + ...combatMethods, + ...jobMethods, + ...runEndMethods, + ...renderMethods, + ...rewardMethods, + ...itemMethods, + ...tooltipMethods, + ...mapMethods, + ...shopMethods, ]); for (const m of combat.ContentProto.Json.Methods) { if (m.ExecSpace === 0) m.ExecSpace = 6; // 기본은 ClientOnly(6), 서버 RPC(Server=1·Client=2) 명시값은 보존