-- /*============================================= -- = util = -- =============================================*/ --- checks if string is empty local function string_empty(str) return #str == 0 end local function lineBreaks(input, lineLength, spaceCount) spaceCount = spaceCount or 0 local result = {} local space = string.rep(" ", spaceCount) local inputLen = #input for pos = 1, inputLen, lineLength do local endPos = math.min(pos + lineLength - 1, inputLen) result[#result + 1] = input:sub(pos, endPos) if endPos < inputLen then result[#result + 1] = "\n" .. space end end return table.concat(result) end --- Gets a button widget by ID local function getButtonById(id) if not id then return nil end for _, actionbar in pairs(actionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do if button:getId() == id then return button end end end return nil end --- Checks if a button is empty local function buttonIsEmpty(button) return button.item:getItemId() == 0 and string_empty(button.item.text:getText()) and string_empty(button.item.text:getImageSource()) end --- Gets the name of an action type function getActionName(actionType) for k, v in pairs(UseTypes) do if v == actionType then return k end end end local function resolveMultiUseType(useType) if type(useType) == "string" and tonumber(useType) == nil then return useType end return getActionName(tonumber(useType) or useType) or "Use" end function hasMultiActions(multiActions) if type(multiActions) ~= "table" then return false end for i = 1, 3 do if type(multiActions[i]) == "table" and not table.empty(multiActions[i]) then return true end end return false end --- Persists a single multi-action slot to ApiJson function persistMultiSlot(barId, buttonId, slotIndex, slotData) if not slotData or table.empty(slotData) then ApiJson.removeMultiAction(barId, buttonId, slotIndex) return end if slotData["chatText"] then ApiJson.createOrUpdateMultiText(barId, buttonId, slotIndex, slotData["chatText"], slotData["sendAutomatically"]) elseif slotData["useObject"] then local useTypeName = resolveMultiUseType(slotData["useType"]) ApiJson.createOrUpdateMultiAction(barId, buttonId, slotIndex, useTypeName, slotData["useObject"], slotData["upgradeTier"] or 0, slotData["useEquipSmartMode"] or false) end end --- Gets item name by ID local function getItemNameById(itemId) for _, k in pairs(hotkeyItemList) do local item = k[1] if item:getId() == itemId then return k[2] end end return "this object" end --- Checks if player can use a spell local function playerCanUseSpell(spellData) if not g_game.isOnline() then return end if not spellData then return false end if spellData.needLearn and not spellListData[tostring(spellData.id)] then return false end if spellData.mana and (player:getMana() < spellData.mana) then return false end if spellData.level and (player:getLevel() < spellData.level) then return false end if spellData.soul and (player:getSoul() < spellData.soul) then return false end if spellData.vocations and (not table.contains(spellData.vocations, translateVocation(player:getVocation()))) then return false end return true end -- /*============================================= -- = Hotkeys = -- =============================================*/ local function onCheckKeyUp(button) local cache = getButtonCache(button) if cache.isSpell then spellGroupPressed[tostring(button.cache.primaryGroup)] = nil end end local function bindHotkey(button, hotkey) if not gameRootPanel or not button or not hotkey or string_empty(hotkey) then return end local combo = hotkey g_keyboard.bindKeyPress(combo, function() if not modules.game_hotkeys.canPerformKeyCombo(combo) then return end onExecuteAction(button, true) end, gameRootPanel) g_keyboard.bindKeyDown(combo, function() if not modules.game_hotkeys.canPerformKeyCombo(combo) then return end onExecuteAction(button, false) end, gameRootPanel) g_keyboard.bindKeyUp(combo, function() if not modules.game_hotkeys.canPerformKeyCombo(combo) then return end onCheckKeyUp(button) end, gameRootPanel) end local hotkeyCache = {} local hotkeyCacheValid = false local cachedChatMode = nil --- Updates the hotkey cache local function updateHotkeyCache() hotkeyCache = {} local currentChatMode = modules.game_console.isChatEnabled() and 'chatOn' or 'chatOff' cachedChatMode = currentChatMode hotkeyCacheValid = true if not ApiJson.hasCurrentHotkeySet() then return end local entries = ApiJson.getHotkeyEntries(currentChatMode) if not entries then return end for _, data in pairs(entries) do if data["actionsetting"] and data["actionsetting"]["action"] then local action = data["actionsetting"]["action"] local keySequence = data["keysequence"] if keySequence and not string_empty(keySequence) then hotkeyCache[action] = keySequence end end end end --- Clears the hotkey cache function clearHotkeyCache() hotkeyCache = {} hotkeyCacheValid = false cachedChatMode = nil end --- Sets up hotkey for a button local function setupHotkeyButton(button) if not ApiJson.hasCurrentHotkeySet() then return end local currentChatMode = modules.game_console.isChatEnabled() and 'chatOn' or 'chatOff' -- Invalidate/rebuild cache if needed (you might want a better invalidation strategy later) -- For now, we rebuild if it's empty, or we could expose a function to clear it if not hotkeyCacheValid or cachedChatMode ~= currentChatMode then updateHotkeyCache() end local actionKey = "TriggerActionButton_" .. button:getId() local keySequence = hotkeyCache[actionKey] if keySequence then button.cache.hotkey = keySequence unbindHotkey(keySequence) bindHotkey(button, keySequence) end end local function executeSpecialAction(specialActionId) if not specialActionId then return end if specialActionId == "toggleWasdChatMode" then modules.game_console.toggleChat() elseif specialActionId == "attackNext" then modules.game_battle.attackNext() elseif specialActionId == "attackPrevious" then modules.game_battle.attackNext(true) elseif specialActionId == "toggleChase" then modules.game_hotkeys.toggleChaseMode() end end local function use_item_at_cursor_position(button) if not button or not button.item then return false end local item = button.item:getItem() if not item then return false end local subType = button.item:getItemSubType() or -1 local function safeStartUseWith() modules.game_interface.startUseWith(item, subType) return false end local mapPanel = modules.game_interface and modules.game_interface.getMapPanel and modules.game_interface.getMapPanel() if not mapPanel then return safeStartUseWith() end local mousePosition = g_window.getMousePosition() if not mapPanel:containsPoint(mousePosition) then return safeStartUseWith() end local mapPosition = mapPanel:getPosition(mousePosition) if not mapPosition then return safeStartUseWith() end local localPlayer = g_game.getLocalPlayer() if localPlayer and mapPosition.z ~= localPlayer:getPosition().z then local dz = mapPosition.z - localPlayer:getPosition().z mapPosition.x = mapPosition.x + dz mapPosition.y = mapPosition.y + dz mapPosition.z = localPlayer:getPosition().z end local tile = g_map.getTile(mapPosition) if not tile then return safeStartUseWith() end local useThing = nil if item:isFluidContainer() or item:isMultiUse() then useThing = tile:getTopMultiUseThing() else useThing = tile:getTopUseThing() end if not useThing then return safeStartUseWith() end g_game.useWith(item, useThing, subType) return true end -- /*============================================= -- = button behavior = -- =============================================*/ --- Executes the action assigned to a button function onExecuteAction(button, isPress) local cache = getButtonCache(button) if cache.lastClick > g_clock.millis() then return true end if modules.game_interface.getMainRightPanel():isFocusable() or modules.game_interface.getLeftPanel():isFocusable() then return true end if not isPress then button.cache.nextDownKey = g_clock.millis() + 500 end if isPress and button.cache.nextDownKey > g_clock.millis() then return true end local cooldown = isPress and 600 or 150 button.cache.lastClick = g_clock.millis() + cooldown local action = button.cache.actionType if action == 0 then return true end if action == UseTypes["Equip"] and button.item then local tier = 0 if g_game.getFeature(GameThingUpgradeClassification) then tier = button.cache.upgradeTier end if player:getInventoryCount(button.cache.itemId, tier) == 0 then return end g_game.equipItemId(button.cache.itemId, tier) end if action == UseTypes["Use"] and button.item then if (button.item:getItem():isContainer()) then g_game.closeContainerByItemId(button.item:getItemId()) else g_game.useInventoryItem(button.item:getItemId()) end end if action == UseTypes["UseOnYourself"] and button.item then g_game.useInventoryItemWith(button.item:getItemId(), player, button.item:getItemSubType() or -1) if not g_game.getFeature(GameEnterGameShowAppearance) then -- temp old protocol updateInventoryItems() end end if button.item then if action == UseTypes["SelectUseTarget"] then modules.game_interface.startUseWith(button.item:getItem(), button.item:getItemSubType() or -1) end if action == UseTypes["UseAtCursorPosition"] then use_item_at_cursor_position(button) end if action == UseTypes["UseOnTarget"] then local attackingCreature = g_game.getAttackingCreature() if not attackingCreature then modules.game_interface.startUseWith(button.item:getItem(), button.item:getItemSubType() or -1) else g_game.useWith(button.item:getItem(), attackingCreature, button.item:getItemSubType() or -1) end end end if action == UseTypes["chatText"] and button.cache.sendAutomatic then if button.cache.isSpell then spellGroupPressed[tostring(button.cache.primaryGroup)] = true -- OT-MMO: con un aliado elegido en el marco del grupo, la curación va a él (game_grupo). g_game.talk(modules.game_grupo and modules.game_grupo.conAliado(button.cache.param) or button.cache.param) else modules.game_console.sendMessage(button.cache.param) end modules.game_console.getConsole():setText('') elseif action == UseTypes["specialAction"] then executeSpecialAction(button.cache.specialAction) elseif action == UseTypes["chatText"] then modules.game_console.getConsole():setText(button.cache.param) modules.game_console.getConsole():setCursorPos(#button.cache.param) end end --- Translates hotkey text for display local function translateDisplayHotkey(text) if HotkeyShortcuts[text] then text = HotkeyShortcuts[text] elseif string.len(text) > 5 then text = "..." .. string.sub(text, string.len(text) - 2, string.len(text)) end return text end function clearButton(button, removeAction) local hotkey = button.cache.hotkey if button.cache.cooldownEvent then removeEvent(button.cache.cooldownEvent) end if cacheMultiActionButtons then cacheMultiActionButtons[button] = nil end if clearMultiActionCooldownEvents then clearMultiActionCooldownEvents(button:getId()) end removeCooldown(button) resetButtonCache(button) if hotkey then button.cache.hotkey = hotkey button.hotkeyLabel:setText(translateDisplayHotkey(button.cache.hotkey)) end setupButtonTooltip(button, true) if removeAction then local barID, buttonID = string.match(button:getId(), "(.*)%.(.*)") ApiJson.removeAction(tonumber(barID), tonumber(buttonID)) end end --- Updates the state of a button function updateButtonState(button) if not button then return end if not player then player = g_game.getLocalPlayer() end if not player then return end if not button.item then return end button:recursiveGetChildById('activeSpell'):setVisible(false) if button.cache.isSpell then setupButtonTooltip(button, false) button.item.text.gray:setVisible(not playerCanUseSpell(button.cache.spellData)) local spellId = 0 button:recursiveGetChildById('activeSpell'):setVisible(button.cache.spellData.id == spellId) elseif button.cache.itemId ~= 0 then local tier = 0 if g_game.getFeature(GameThingUpgradeClassification) then tier = button.cache.upgradeTier end local isItemEquipped = player:hasEquippedItemId(button.cache.itemId, tier) local itemCount = player:getInventoryCount(button.cache.itemId, tier) if g_game.getFeature(GameEnterGameShowAppearance) then -- fix old protocol if button.cache.actionType == UseTypes["Equip"] then button.item:setChecked(itemCount ~= 0 and isItemEquipped) end button.item.gray:setVisible(itemCount == 0) end if modules.client_options.getOption('showHKObjectsBars') then button.item:setDisplayCount(itemCount) else button.item:setDisplayCount(0) end setupButtonTooltip(button, false) end end --- Gets or creates the cache for a button function getButtonCache(button) if not button then return { cooldownEvent = nil, cooldownTime = 0, isSpell = false, isRuneSpell = false, isPassive = false, spellID = 0, spellData = nil, specialAction = nil, param = "", sendAutomatic = false, actionType = 0, upgradeTier = 0, hotkey = nil, lastClick = 0, nextDownKey = 0, isDragging = false, buttonIndex = 0, buttonParent = nil, itemId = 0, multiActions = {{}, {}, {}}, smartMode = false } end if not button.cache then button.cache = { cooldownEvent = nil, cooldownTime = 0, isSpell = false, isRuneSpell = false, isPassive = false, spellID = 0, spellData = nil, specialAction = nil, param = "", sendAutomatic = false, actionType = 0, upgradeTier = 0, hotkey = nil, lastClick = 0, nextDownKey = 0, isDragging = false, buttonIndex = 0, buttonParent = nil, itemId = 0, multiActions = {{}, {}, {}}, smartMode = false } end return button.cache end --- Resets the cache of a button function resetButtonCache(button) -- Optimize: Check if we really need to clear the item widget cache if button.cache and button.cache.itemId > 0 then local cachedItem = cachedItemWidget[button.cache.itemId] if cachedItem then for index, widget in pairs(cachedItem) do if button == widget then table.remove(cachedItem, index) break end end end end if button.item then if button.item:getItemId() ~= 0 then button.item:setItemId(0) end if button.item:isOn() then button.item:setOn(false) end if button.item:isChecked() then button.item:setChecked(false) end if button.item:isDraggable() then button.item:setDraggable(false) end if button.item.gray and button.item.gray:isVisible() then button.item.gray:setVisible(false) end if button.item.text then if button.item.text.gray and button.item.text.gray:isVisible() then button.item.text.gray:setVisible(false) end button.item.text:setImageSource('') button.item.text:setText('') end end -- Text updates if button.hotkeyLabel then button.hotkeyLabel:setText('') end if button.parameterText then button.parameterText:setText('') end if button.cooldown then button.cooldown:setPercent(100) button.cooldown:setText("") end -- Event cleanup if button.cache and button.cache.removeCooldownEvent then removeEvent(button.cache.removeCooldownEvent) button.cache.removeCooldownEvent = nil end if not button.cache then button.cache = {} end -- Reset fields local c = button.cache c.cooldownEvent = nil c.cooldownTime = 0 c.isSpell = false c.isRuneSpell = false c.isPassive = false c.spellID = 0 c.spellData = nil c.primaryGroup = nil c.specialAction = nil c.param = "" c.sendAutomatic = false c.actionType = 0 c.upgradeTier = 0 c.hotkey = nil c.lastClick = 0 c.nextDownKey = 0 c.isDragging = false c.buttonIndex = 0 c.buttonParent = nil c.itemId = 0 c.multiActions = {{}, {}, {}} c.smartMode = false if button.multiIcon then button.multiIcon:setVisible(false) end end -- /*============================================= -- = Tooltip = -- =============================================*/ --- Sets up the tooltip for a button function setupButtonTooltip(button, isEmpty) if not g_game.isOnline() then return true end local cache = getButtonCache(button) if isEmpty then -- OT-MMO (23-oct): textos en español; cómo llenarla (arrastrar o clic derecho) local hotkeyDesc = cache.hotkey ~= nil and cache.hotkey or "sin tecla" local tooltip = "Casilla vacía (tecla: " .. hotkeyDesc .. ")" tooltip = tooltip .. "\nArrastra aquí un hechizo del libro (P) o un objeto de las bolsas (B)." tooltip = tooltip .. "\nClic derecho: asignar o cambiar la tecla." if button.item then button.item:setTooltip(tooltip) end return true end local actionDesc = "" local spellData = cache.spellData local function spellStatsTooltip(data) if not data then return "" end local cooldown = ((data.exhaustion or 0) / 1000) local mana = data.mana or 0 return "Recarga: " .. cooldown .. " s\n" .. "Maná: " .. mana end if cache.actionType == UseTypes["chatText"] then if not cache.isSpell then actionDesc = 'Decir: "' .. lineBreaks(cache.param, 44, 36) .. '"\n' actionDesc = actionDesc .. "Se envía solo: " .. (cache.sendAutomatic and "sí" or "no") else actionDesc = Spells.getSpellNameByWords(spellData.words) .. "\n" actionDesc = actionDesc .. "Palabras: " .. cache.param .. "\n" actionDesc = actionDesc .. spellStatsTooltip(spellData) end elseif cache.actionType == UseTypes["specialAction"] then local specialAction = getActionBarSpecialAction(cache.specialAction) actionDesc = specialAction and specialAction.text or "Unknown action" elseif cache.actionType == UseTypes["passiveAbility"] then actionDesc = "Gift of Life" else actionDesc = UseTypesTip[cache.actionType] if actionDesc == nil then actionDesc = "Use %s" end if cache.actionType == UseTypes["Equip"] and button.item then local itemName = getItemNameById(button.item:getItem():getId()) .. ((cache.upgradeTier and cache.upgradeTier > 0) and " (Tier " .. cache.upgradeTier .. ")" or "") actionDesc = tr(actionDesc, (button.item:isChecked() and "Quitarte" or "Equipar"), itemName) elseif button.item and button.item:getItem() then actionDesc = tr(actionDesc, getItemNameById(button.item:getItem():getId())) end local itemCount = player:getInventoryCount(button.cache.itemId, button.cache.upgradeTier) actionDesc = actionDesc .. "\nCantidad: " .. itemCount if cache.isRuneSpell and spellData then actionDesc = actionDesc .. "\n" .. spellStatsTooltip(spellData) end end local hotkeyDesc = cache.hotkey ~= nil and cache.hotkey or "sin tecla" local tooltip = "" if cache.actionType == UseTypes["passiveAbility"] then tooltip = tooltip .. "\n\n Passive Ability: " .. actionDesc tooltip = tooltip .. "\n Hotkeys: " .. hotkeyDesc else tooltip = actionDesc .. "\nTecla: " .. hotkeyDesc end if button.item then button.item:setTooltip(tooltip) end end -- /*============================================= -- = Animation Cooldown = -- =============================================*/ function checkRemainSpellCooldown(button, spellId) if not modules.client_options.getOption("graphicalCooldown") and not modules.client_options.getOption("cooldownSecond") then return true end local cooldownData = spellCooldownCache[spellId] if not cooldownData then return end if (cooldownData.startTime + cooldownData.exhaustion) < g_clock.millis() then return end button.cache = getButtonCache(button) local remainTime = (cooldownData.startTime + cooldownData.exhaustion) - g_clock.millis() updateCooldown(button, remainTime) if button.cache.removeCooldownEvent then removeEvent(button.cache.removeCooldownEvent) button.cache.removeCooldownEvent = nil end button.cache.removeCooldownEvent = scheduleEvent(function() removeCooldown(button) end, remainTime) end function removeCooldown(button) if not button or not button.cache then return true end button.cache.removeCooldownEvent = nil if button.cooldown then button.cooldown:stop() button.cooldown:setPercent(100) button.cooldown:setText("") end end function updateCooldown(button, timeMs) button.cooldown:showTime(modules.client_options.getOption("cooldownSecond")) button.cooldown:showProgress(modules.client_options.getOption("graphicalCooldown")) button.cooldown:setDuration(timeMs) button.cooldown:start() end function updateActionPassive(button) if not modules.client_options.getOption("graphicalCooldown") and not modules.client_options.getOption("cooldownSecond") then return true end if not button then for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do local cache = button.cache if cache and cache.isPassive then button.item.text.gray:setVisible(passiveData.max == 0) if cache.cooldownEvent == nil then updateCooldown(button, passiveData.cooldown * 1000) if cache.removeCooldownEvent then removeEvent(cache.removeCooldownEvent) cache.removeCooldownEvent = nil end cache.removeCooldownEvent = scheduleEvent(function() removeCooldown(button) end, passiveData.cooldown * 1000) end end end end return true else if button.cache.isPassive then button.item.text.gray:setVisible(passiveData.max == 0) end end if passiveData.max > 0 then if button.cache.removeCooldownEvent then removeEvent(button.cache.removeCooldownEvent) button.cache.removeCooldownEvent = nil end updateCooldown(button, passiveData.cooldown * 1000) button.cache.removeCooldownEvent = scheduleEvent(function() removeCooldown(button) end, passiveData.cooldown * 1000) end end -- /*============================================= -- = right button in an action bar slot = -- =============================================*/ function configureButtonMouseRelease(button) button.onMouseRelease = function(button, mousePos, mouseButton) button.cache = getButtonCache(button) if mouseButton == MouseRightButton then local menu = g_ui.createWidget('PopupMenu') menu:setGameMenu(true) menu:addOption(button.cache.isSpell and 'Cambiar hechizo' or 'Asignar hechizo', function() assignSpell(button) end) if button.item and button.item:getItemId() > 100 then menu:addOption('Cambiar objeto', function() assignItem(button, button.item:getItemId()) end) else menu:addOption('Asignar objeto', function() assignItemEvent(button) end) end local buttonText = "" if button.item then buttonText = button.item.text:getText() end menu:addOption(buttonText:len() > 0 and 'Cambiar texto' or 'Asignar texto', function() assignText(button) end) menu:addOption(button.cache.isPassive and 'Cambiar pasiva' or 'Asignar pasiva', function() assignPassive(button) end) menu:addOption(button.cache.specialAction and 'Cambiar acción' or 'Asignar acción', function() assignSpecialAction(button, mousePos) end) menu:addOption(button.cache.hotkey and 'Cambiar tecla' or 'Asignar tecla', function() assignHotkey(button) end) local buttonHasMulti = hasMultiActions(button.cache.multiActions) local hasMultiIcon = button.multiIcon and button.multiIcon:isVisible() local hasMultiActions = buttonHasMulti if assignMultiAction then local panelOpen = multiPanel and not multiPanel:isDestroyed() and multiPanel.button == button if panelOpen then menu:addOption('Cerrar multiacción', function() closeCurrentMultiActionPanel() end) else menu:addOption((hasMultiActions or hasMultiIcon) and 'Cambiar multiacción' or 'Asignar multiacción', function() assignMultiAction(button) end) end end if button.cache.actionType > 0 or hasMultiActions then menu:addSeparator() menu:addOption('Quitar de la barra', function() if closeCurrentMultiActionPanel and multiPanel and multiPanel.button == button then closeCurrentMultiActionPanel() end local barID, buttonID = string.match(button:getId(), "(.*)%.(.*)") if hasMultiActions then for i = 1, 3 do ApiJson.removeMultiAction(tonumber(barID), tonumber(buttonID), i) end end clearButton(button, true) end) end if button.item and button.item:getItemId() > 100 then if modules.game_bot then menu:addSeparator() local useThingId = button.item:getItemId() menu:addOption("ID: " .. useThingId, function() g_window.setClipboardText(useThingId) end) end end menu:display(mousePos) end end end -- /*============================================= -- = click left in the action bar slot = -- =============================================*/ --- Updates the button's visual representation function updateButton(button) local startUpdate = g_clock.millis() if not player then player = g_game.getLocalPlayer() end local barID, buttonID = string.match(button:getId(), "(%d+)%.(%d+)") local barIndex = tonumber(barID) local buttonIndex = tonumber(buttonID) local buttonData = nil if not button.item then local actionId, buttonId = button:getId():match("([^.]+)%.([^.]+)") button:destroy() local actionbar = actionBars[tonumber(actionId)] local layout = tonumber(actionId) < 4 and 'ActionButton' or 'SideActionButton' local widget = g_ui.createWidget(layout, actionbar.tabBar) actionbar.tabBar:moveChildToIndex(widget, tonumber(buttonId)) widget:setId(actionId .. "." .. buttonId) updateButton(widget) return end if button.multiPanel then if closeCurrentMultiActionPanel and multiPanel and button.multiPanel == multiPanel then closeCurrentMultiActionPanel() else button.onGeometryChange = nil button.onVisibilityChange = nil if not button.multiPanel:isDestroyed() then button.multiPanel:destroy() end button.multiPanel = nil end end buttonData = ApiJson.getMapping(barIndex, buttonIndex) local isClean = button.cache and button.cache.actionType == 0 and button.item:getItemId() == 0 local hasNewData = buttonData and buttonData["actionsetting"] if isClean and not hasNewData then setupHotkeyButton(button) if button.cache.hotkey then button.item.text:setTextOffset("0 8") button.hotkeyLabel:setText(translateDisplayHotkey(button.cache.hotkey)) else button.hotkeyLabel:setText('') -- ensure cleared end setupButtonTooltip(button, true) button.item:setDraggable(false) configureButtonMouseRelease(button) return true end resetButtonCache(button) button.item.text:setTextOffset("0 0") button.cache = getButtonCache(button) -- Ensure cache is grabbed (resetButtonCache ensures it exists) if button.item.getItemId and not button.cache.actionType then if button.item:getItemId() ~= 0 then -- Optimization check button.item:setItemId(0, true) end if button.item:isOn() then button.item:setOn(false) end end setupHotkeyButton(button) if button.cache.hotkey then button.item.text:setTextOffset("0 8") button.hotkeyLabel:setText(translateDisplayHotkey(button.cache.hotkey)) end if not buttonData or not buttonData["actionsetting"] then local startTips = g_clock.millis() setupButtonTooltip(button, true) local tipsTime = g_clock.millis() - startTips button.item:setDraggable(false) configureButtonMouseRelease(button) return true end local useAction = buttonData["actionsetting"]["useObject"] local sendText = buttonData["actionsetting"]["chatText"] local passiveAbility = buttonData["actionsetting"]["passiveAbility"] local specialAction = buttonData["actionsetting"]["specialAction"] local multiActions = buttonData["actionsetting"]["multiActions"] local hasMultiData = type(multiActions) == "table" and hasMultiActions(multiActions) or false if hasMultiData then button.cache.multiActions = {{}, {}, {}} for i = 1, 3 do if type(multiActions[i]) == "table" then button.cache.multiActions[i] = multiActions[i] end end if button.multiIcon then button.multiIcon:setVisible(true) end if cacheMultiActionButtons then cacheMultiActionButtons[button] = true end if updateMultiButtonState then updateMultiButtonState(button) end if registerMultiActionCooldownEvents then registerMultiActionCooldownEvents(button) end button.item:setDraggable(true) local parentButton = button:getParent() if parentButton then button.cache.buttonIndex = parentButton:getChildIndex(button) button.cache.buttonParent = parentButton end local barIndexLocal = barIndex button.item.onDragEnter = function(self, mousePos) if ApiJson.isBarLocked(barIndexLocal) then return false end button.cooldown:setBorderWidth(1) button.cache.isDragging = true dragButton = button dragItem = self return true end button.item.onDragMove = function(self, mousePos) self:setPhantom(true) self:setParent(gameRootPanel) self:setX(mousePos.x) self:setY(mousePos.y) self:setBorderColor('white') if lastHighlightWidget then lastHighlightWidget:setBorderWidth(0) lastHighlightWidget:setBorderColor('alpha') end local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePos, false) if not clickedWidget or not clickedWidget:backwardsGetWidgetById("tabBar") then return true end lastHighlightWidget = clickedWidget lastHighlightWidget:setBorderWidth(1) lastHighlightWidget:setBorderColor('white') end button.item.onDragLeave = function(self, widget, mousePos) if not button.cache.isDragging then return false end isLoaded = false button.cache.isDragging = false onDragItemLeave(self, mousePos, button) isLoaded = true dragButton = nil dragItem = nil end button.item.onClick = function() onExecuteAction(button) end button.item.text.onClick = function() onExecuteAction(button) end configureButtonMouseRelease(button) return true end if useAction then button.item:setItemId(useAction, true) button.item:setOn(true) local cached = cachedItemWidget[useAction] if cached then table.insert(cached, button) else cachedItemWidget[useAction] = {} table.insert(cachedItemWidget[useAction], button) end local spellData = Spells.getRuneSpellByItem(useAction) if spellData then button.cache.isRuneSpell = true button.cache.spellData = spellData if spellData.vocations and not table.contains(spellData.vocations, translateVocation(player:getVocation())) then button.item.gray:setVisible(true) end end button.cache.itemId = button.item:getItemId() button.cache.upgradeTier = buttonData["actionsetting"]["upgradeTier"] local useTypeName = buttonData["actionsetting"]["useType"] button.cache.actionType = UseTypes[useTypeName] or UseTypes["Use"] ItemsDatabase.setTier(button.item, button.cache.upgradeTier) updateButtonState(button) end if sendText then local spellData, param = Spells.getSpellDataByParamWords(sendText:lower()) if spellData then local spellId = spellData.clientId if not spellId then print("Warning Spell ID not found L734 modules/game_actionbar/logics/ActionButtonLogic.lua") return end local source = SpelllistSettings['Default'].iconFile local clip = Spells.getImageClip(spellId, 'Default') button.item.text:setImageSource(source) button.item.text:setImageClip(clip) button.cache.isSpell = true button.cache.spellID = spellData.id button.cache.spellData = spellData local groupIds = Spells.getGroupIds(spellData) button.cache.primaryGroup = groupIds and groupIds[1] or nil if param then local formatedParam = param:gsub('"', '') button.parameterText:setText(short_text('"' .. formatedParam, 4)) button.cache.castParam = formatedParam end if not playerCanUseSpell(spellData) then button.item.text.gray:setVisible(true) end checkRemainSpellCooldown(button, spellData.id) else button.item.text:setText(short_text(sendText, 15)) end button.item:setOn(true) button.cache.param = sendText button.cache.sendAutomatic = buttonData["actionsetting"]["sendAutomatically"] button.cache.actionType = UseTypes["chatText"] end if passiveAbility then local passive = PassiveAbilities[passiveAbility] button.item.text:setImageSource(passive.icon) button.item.text:setImageClip("0 0 32 32") button.cache.actionType = UseTypes["passiveAbility"] button.cache.isPassive = true updateActionPassive(button) end if specialAction then local specialActionData = getActionBarSpecialAction(specialAction) button.item.text:setText(short_text(specialActionData and specialActionData.text or specialAction, 15)) button.item:setOn(true) button.cache.specialAction = specialAction button.cache.actionType = UseTypes["specialAction"] end button.item:setDraggable(true) setupButtonTooltip(button, false) local parentButton = button:getParent() if parentButton then button.cache.buttonIndex = parentButton:getChildIndex(button) button.cache.buttonParent = parentButton end button.item.onDragEnter = function(self, mousePos) if ApiJson.isBarLocked(barIndex) then return false end button.cooldown:setBorderWidth(1) button.cache.isDragging = true dragButton = button dragItem = self return true end button.item.onDragMove = function(self, mousePos) self:setPhantom(true) self:setParent(gameRootPanel) self:setX(mousePos.x) self:setY(mousePos.y) self:setBorderColor('white') if lastHighlightWidget then lastHighlightWidget:setBorderWidth(0) lastHighlightWidget:setBorderColor('alpha') end local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePos, false) if not clickedWidget or not clickedWidget:backwardsGetWidgetById("tabBar") then return true end lastHighlightWidget = clickedWidget lastHighlightWidget:setBorderWidth(1) lastHighlightWidget:setBorderColor('white') end button.item.onDragLeave = function(self, widget, mousePos) if not button.cache.isDragging then return false end isLoaded = false button.cache.isDragging = false onDragItemLeave(self, mousePos, button) isLoaded = true dragButton = nil dragItem = nil end button.item.onClick = function() onExecuteAction(button) end button.item.text.onClick = function() onExecuteAction(button) end if button.multiIcon then button.multiIcon:setVisible(false) end configureButtonMouseRelease(button) ActionBarController:scheduleEvent(function() onMultiUseCooldown() end, 100) end -- /*============================================= -- = Mouse Drag Event = -- =============================================*/ -- item in UIMap or UIWidget(UIItem) drop in slot actionbar --- Gets button from widget local function getButtonFromWidget(widget) if not widget then return nil end local widgetId = widget:getId() if widgetId and widgetId:match("^%d+%.%d+$") then return widget end return getButtonFromWidget(widget:getParent()) end --- Resolves dropped item data local function resolveDroppedItemData(draggedWidget, item) if type(item) == 'number' then local itemTier = 0 if draggedWidget and draggedWidget.getItem then local draggedItem = draggedWidget:getItem() if draggedItem and draggedItem.getTier then itemTier = draggedItem:getTier() or 0 end end return item, itemTier end if item and item.getId then local itemId = item:getId() local itemTier = item.getTier and (item:getTier() or 0) or 0 return itemId, itemTier end return nil, 0 end --- Tries to assign action from a drop event function tryAssignActionButtonFromDrop(mousePos, draggedWidget, item) if not hasAnyActiveActionBar() or not item then return false end if dragButton or not draggedWidget or not gameRootPanel then return false end local className = draggedWidget:getClassName() if className ~= 'UIItem' and className ~= 'UIGameMap' then return false end if className == 'UIItem' then local parentWidget = draggedWidget:getParent() if parentWidget and parentWidget:getId() == 'actionBarPanel' then return false end end local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePos, false) if not clickedWidget then return false end if clickedWidget:getParent() then local parentId = clickedWidget:getParent():getId() or "" local targetIndex = tonumber(string.match(parentId, "^actionButton(%d)$")) if targetIndex and targetIndex >= 1 and targetIndex <= 3 then local panel = clickedWidget:getParent():getParent() if panel and panel.button then local itemId, itemTier = resolveDroppedItemData(draggedWidget, item) if not itemId then return false end local thingType = g_things.getThingType(itemId, ThingCategoryItem) if not thingType or not thingType:isPickupable() then return false end if assignMultiItem then assignMultiItem(panel.button, targetIndex, itemId, itemTier, true) return true end return false end end end local tabBar = clickedWidget:backwardsGetWidgetById("tabBar") if not tabBar or not tabBar:isVisible() then return false end local button = getButtonFromWidget(clickedWidget) if not button or not button:isVisible() then return false end local actionBar = tabBar:getParent() if not actionBar or not actionBar:isVisible() then return false end local itemId, itemTier = resolveDroppedItemData(draggedWidget, item) if not itemId then return false end local thingType = g_things.getThingType(itemId, ThingCategoryItem) if not thingType or not thingType:isPickupable() then return false end assignItem(button, itemId, itemTier) return true end -- move button to other slot bar --- Resets a dragging widget function resetDragWidget(self, button) button.cache = getButtonCache(button) local cachedItem = cachedItemWidget[button.cache.itemId] if cachedItem then for index, widget in pairs(cachedItem) do if button == widget then table.remove(cachedItem, index) end end end self:destroy() local barID, buttonID = string.match(button:getId(), "(.*)%.(.*)") local style = tonumber(barID) > 3 and "SideActionButton" or "ActionButton" button:destroy() local destBar = actionBars[tonumber(barID)].tabBar local widget = g_ui.createWidget(style, destBar) if destBar then destBar:moveChildToIndex(widget, buttonID) end widget:setId(barID .. "." .. buttonID) updateButton(widget) end --- Handles drag item leave event function onDragItemLeave(self, mousePos, button) if lastHighlightWidget then lastHighlightWidget:setBorderWidth(0) lastHighlightWidget:setBorderColor('alpha') end button.cache = getButtonCache(button) local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePos, false) if clickedWidget and clickedWidget:getParent() then local parentId = clickedWidget:getParent():getId() or "" local targetIndex = tonumber(string.match(parentId, "^actionButton(%d)$")) if targetIndex and targetIndex >= 1 and targetIndex <= 3 then local panel = clickedWidget:getParent():getParent() if panel and panel.button then local targetButton = panel.button if panel.button == button then resetDragWidget(self, button) return true end local tBarID, tButtonID = string.match(targetButton:getId(), "(.*)%.(.*)") local sourceBarID, sourceButtonID = string.match(button:getId(), "(.*)%.(.*)") if hasMultiActions(button.cache.multiActions) then targetButton.cache = getButtonCache(targetButton) targetButton.cache.multiActions = {{}, {}, {}} for i = 1, 3 do local slotData = button.cache.multiActions[i] or {} if not table.empty(slotData) then targetButton.cache.multiActions[i] = table.copy(slotData) end persistMultiSlot(tonumber(tBarID), tonumber(tButtonID), i, slotData) end elseif button.cache.actionType == UseTypes["chatText"] and button.cache.param and button.cache.param ~= "" then ApiJson.createOrUpdateMultiText(tonumber(tBarID), tonumber(tButtonID), targetIndex, button.cache.param, button.cache.sendAutomatic) targetButton.cache = getButtonCache(targetButton) targetButton.cache.multiActions = targetButton.cache.multiActions or {{}, {}, {}} targetButton.cache.multiActions[targetIndex] = { chatText = button.cache.param, sendAutomatically = button.cache.sendAutomatic } elseif button.cache.itemId and button.cache.itemId > 100 then local useTypeName = resolveMultiUseType(button.cache.actionType) ApiJson.createOrUpdateMultiAction(tonumber(tBarID), tonumber(tButtonID), targetIndex, useTypeName, button.cache.itemId, button.cache.upgradeTier or 0, button.cache.smartMode or false) targetButton.cache = getButtonCache(targetButton) targetButton.cache.multiActions = targetButton.cache.multiActions or {{}, {}, {}} targetButton.cache.multiActions[targetIndex] = { useObject = button.cache.itemId, useType = useTypeName, upgradeTier = button.cache.upgradeTier or 0, useEquipSmartMode = button.cache.smartMode or false } else resetDragWidget(self, button) return true end if targetButton.multiIcon then targetButton.multiIcon:setVisible(true) end if cacheMultiActionButtons then cacheMultiActionButtons[targetButton] = true end ApiJson.removeAction(tonumber(sourceBarID), tonumber(sourceButtonID)) if updateMultiButtonState then updateMultiButtonState(targetButton) end if registerMultiActionCooldownEvents then registerMultiActionCooldownEvents(targetButton) end if assignMultiAction and multiPanel and multiPanel.button == targetButton then assignMultiAction(targetButton, true) end resetDragWidget(self, button) return true end end end if not clickedWidget or not clickedWidget:backwardsGetWidgetById("tabBar") then resetDragWidget(self, button) return true end local destButton = getButtonById(clickedWidget:getParent():getId()) if not destButton then resetDragWidget(self, button) return true end local destButtonCache = destButton.cache button.cache = getButtonCache(button) local itemId = button.cache.itemId local destBarID, destButtonID = string.match(destButton:getId(), "(.*)%.(.*)") local draggedBarID, draggedButtonID = string.match(button:getId(), "(.*)%.(.*)") local sourceHasMulti = hasMultiActions(button.cache.multiActions) if sourceHasMulti then local destHasMulti = destButtonCache and hasMultiActions(destButtonCache.multiActions) or false if destHasMulti then resetDragWidget(self, button) return end local destHasSingleAction = destButtonCache and ((destButtonCache.actionType == UseTypes["chatText"] and destButtonCache.param and destButtonCache.param ~= "") or (destButtonCache.itemId and destButtonCache.itemId > 100)) if destHasSingleAction then resetDragWidget(self, button) return true end for i = 1, 3 do persistMultiSlot(tonumber(destBarID), tonumber(destButtonID), i, button.cache.multiActions[i]) end ApiJson.removeAction(tonumber(draggedBarID), tonumber(draggedButtonID)) if cacheMultiActionButtons then cacheMultiActionButtons[button] = nil end if clearMultiActionCooldownEvents then clearMultiActionCooldownEvents(button:getId()) end updateButton(destButton) resetDragWidget(self, button) self:setBorderColor('alpha') return end local cachedItem = cachedItemWidget[itemId] if cachedItem then for index, widget in pairs(cachedItem) do if button == widget then table.remove(cachedItem, index) end end end local cachedItem = cachedItemWidget[destButtonCache.itemId] if cachedItem then for index, widget in pairs(cachedItem) do if button == widget then table.remove(cachedItem, index) end end end local isButtonEmpty = buttonIsEmpty(destButton) if button.cache.actionType == UseTypes["chatText"] then ApiJson.createOrUpdateText(tonumber(destBarID), tonumber(destButtonID), button.cache.param, button.cache.sendAutomatic) elseif button.cache.actionType == UseTypes["specialAction"] then ApiJson.createOrUpdateSpecialAction(tonumber(destBarID), tonumber(destButtonID), button.cache.specialAction) elseif itemId ~= 0 then ApiJson.createOrUpdateAction(tonumber(destBarID), tonumber(destButtonID), getActionName(button.cache.actionType), itemId, button.cache.upgradeTier) elseif button.cache.isPassive then ApiJson.createOrUpdatePassive(tonumber(destBarID), tonumber(destButtonID), 1) end updateButton(destButton) if isButtonEmpty then ApiJson.removeAction(tonumber(draggedBarID), tonumber(draggedButtonID)) removeCooldown(destButton) resetDragWidget(self, button) else if destButtonCache.actionType == UseTypes["chatText"] then ApiJson.createOrUpdateText(tonumber(draggedBarID), tonumber(draggedButtonID), destButtonCache.param, destButtonCache.sendAutomatic) elseif destButtonCache.actionType == UseTypes["specialAction"] then ApiJson.createOrUpdateSpecialAction(tonumber(draggedBarID), tonumber(draggedButtonID), destButtonCache.specialAction) elseif destButtonCache.itemId ~= 0 then ApiJson.createOrUpdateAction(tonumber(draggedBarID), tonumber(draggedButtonID), getActionName(destButtonCache.actionType), destButtonCache.itemId, destButtonCache.upgradeTier) elseif destButtonCache.isPassive then ApiJson.createOrUpdatePassive(tonumber(draggedBarID), tonumber(draggedButtonID), 1) end removeCooldown(destButton) resetDragWidget(self, button) end self:setBorderColor('alpha') end