ApiJson = dofile('logics/ApiJson.lua') player = nil --localplayer -- @array passiveData = { cooldown = 0, max = 0 } hotkeyItemList = {} --[ItemPtr, desc] spellListData = {} -- [[SpellsIdLearn]] spellCooldownCache = {--[[ [SpellId] = { ["startTime"] = ms, ["exhaustion"] = ms } ]] } spellGroupCooldownCache = {--[[ [GroupId] = { ["startTime"] = ms, ["exhaustion"] = ms } ]] } spellGroupPressed = {--[[ [GroupId] = bool -- Tracks if spell group is currently pressed ]] } cachedItemWidget = { --[[ [ItemID] = { [SlotId] = Widget1, [SlotId] = Widget2, ]] } actionBars = { --[[ -- ActionBarId=1-3: bottom -- ActionBarId=4-6: left -- ActionBarId=7-9: right [ActionBarId] = widget ]] } activeActionBars = {--[[ [VisibleActionBarId] = widget ]] } -- @ widgets dragButton = nil dragItem = nil mouseGrabberWidget = nil gameRootPanel = nil lastHighlightWidget = nil -- @ boolean isLoaded = false local areEventsConnected = false --- checks if action bar is visible local function isActionBarVisible(actionBar) return actionBar and actionBar:isVisible() end --- Adds an action bar to the active list function addActiveActionBar(actionBar) if not actionBar then return false end for _, activeBar in ipairs(activeActionBars) do if activeBar == actionBar then return false end end table.insert(activeActionBars, actionBar) return true end --- Rebuilds the list of active action bars local function rebuildActiveActionBars() local previousCount = #activeActionBars local visibleCount = 0 for _, actionBar in ipairs(actionBars) do if isActionBarVisible(actionBar) then visibleCount = visibleCount + 1 activeActionBars[visibleCount] = actionBar end end for index = visibleCount + 1, previousCount do activeActionBars[index] = nil end return visibleCount > 0 end --- Removes an action bar from the active list function removeActiveActionBar(actionBar) if not actionBar then return false end local removed = false local index = 1 while index <= #activeActionBars do if activeActionBars[index] == actionBar then table.remove(activeActionBars, index) removed = true else index = index + 1 end end return removed end --- Checks if there are any active action bars function hasAnyActiveActionBar() if not g_game.isOnline() then return false end if rebuildActiveActionBars() then return true end return false end --- Sets up an action bar by index function setupActionBar(n) local actionbar = actionBars[n] local barState = ApiJson.getActionBar(n) or {} local visible = barState.isVisible and true or false actionbar:setVisible(visible) actionbar:setOn(visible) local locked = barState.isLocked and true or false actionbar.tabBar.onMouseWheel = nil actionbar.locked = locked local items = {} for i = 1, 50 do local layout = n < 4 and 'ActionButton' or 'SideActionButton' local widget = actionbar.tabBar:getChildById(n .. "." .. i) if not widget then local hasMapping = ApiJson.getMapping(n, i) -- OT-MMO (23-oct): las 10 casillas de las barras de abajo son botones de verdad aunque estén vacías (tecla -- visible, destino de arrastre y menú). El hueco de Tibia era una imagen de 34 px que no casaba con el HUD. if hasMapping or (n <= 3 and i <= 10) then widget = g_ui.createWidget(layout, actionbar.tabBar) widget:setId(n .. "." .. i) else widget = g_ui.createWidget('UIWidget', actionbar.tabBar) widget:setId(n .. "." .. i) widget:setSize({width = 34, height = 34}) widget:setMarginLeft(2) widget:setImageSource('/images/game/actionbar/actionbarslot') widget:setDraggable(false) widget.onDrop = function(self, draggedWidget, mousePos) if draggedWidget and draggedWidget.currentDragThing then if tryAssignActionButtonFromDrop(mousePos, draggedWidget, draggedWidget.currentDragThing) then return true end end end widget.onMouseRelease = function(self, mousePos, mouseButton) if mouseButton == MouseRightButton then local parent = self:getParent() local id = self:getId() updateButton(self) local newButton = parent:getChildById(id) if newButton and newButton.onMouseRelease then newButton.onMouseRelease(newButton, mousePos, mouseButton) end return true end end end end -- Only update if it's a real button if widget:getChildById('item') and g_game.isOnline() then updateButton(widget) end if widget.cooldown then widget.cooldown:stop() end if widget.item and widget.item:getItemId() > 100 then table.insert(items, widget.item:getItem()) end end end --- Gets the count of active bottom action bars function getActiveBottomBars() if #actionBars == 0 then return 0 end local count = 0 for i = 1, 3 do local state = ApiJson.getActionBar(i) or {} if state.isVisible then count = count + 1 end end return count end --- Gets the count of active right action bars function getActiveRightBars() if #actionBars == 0 then return 0 end local count = 0 for i = 7, 9 do local state = ApiJson.getActionBar(i) or {} if state.isVisible then count = count + 1 end end return count end function getActiveLeftBars() if #actionBars == 0 then return 0 end local count = 0 for i = 4, 6 do local state = ApiJson.getActionBar(i) or {} if state.isVisible then count = count + 1 end end return count end local function onUpdateActionBarStatus() if #activeActionBars == 0 then return true end for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do updateButtonState(button) end end end -- /*============================================= -- = Event = -- =============================================*/ --- Connects player events local function connecting() if areEventsConnected then return end connect(LocalPlayer, { onManaChange = onUpdateActionBarStatus, onSoulChange = onUpdateActionBarStatus, onLevelChange = onUpdateLevel, onSpellsChange = onSpellsChange }) connect(g_game, { onItemInfo = onHotkeyItems, onPassiveData = onPassiveData, onSpellCooldown = onSpellCooldown, onMultiUseCooldown = onMultiUseCooldown, onSpellGroupCooldown = onSpellGroupCooldown, updateInventoryItems = updateInventoryItems }) areEventsConnected = true end --- Disconnects player events local function disconnecting() if not areEventsConnected then return end disconnect(LocalPlayer, { onManaChange = onUpdateActionBarStatus, onSoulChange = onUpdateActionBarStatus, onLevelChange = onUpdateLevel, onSpellsChange = onSpellsChange }) disconnect(g_game, { onItemInfo = onHotkeyItems, onPassiveData = onPassiveData, onSpellCooldown = onSpellCooldown, onMultiUseCooldown = onMultiUseCooldown, onSpellGroupCooldown = onSpellGroupCooldown, updateInventoryItems = updateInventoryItems }) areEventsConnected = false end --- Updates event subscriptions based on active bars function updateActionBarEventSubscriptions() if hasAnyActiveActionBar() then connecting() return end disconnecting() end -- /*============================================= -- = Controller = -- =============================================*/ ActionBarController = Controller:new() local function cleanupMultiActionState() if closeCurrentMultiActionPanel then closeCurrentMultiActionPanel() end if multiActionCooldownEvents then for buttonId, events in pairs(multiActionCooldownEvents) do for _, eventId in pairs(events) do removeEvent(eventId) end multiActionCooldownEvents[buttonId] = nil end end cacheMultiActionButtons = {} if pendingMultiButtonUpdate then removeEvent(pendingMultiButtonUpdate) pendingMultiButtonUpdate = nil pendingMultiButtons = {} pendingMultiButtonsTracker = {} end end --- Initializes the action bar controller function ActionBarController:onInit() g_ui.importStyle("otui/style.otui") g_ui.importStyle("otui/multiaction.otui") gameRootPanel = modules.game_interface.getRootPanel() mouseGrabberWidget = g_ui.createWidget('UIWidget') mouseGrabberWidget:setVisible(false) mouseGrabberWidget:setFocusable(false) mouseGrabberWidget.onMouseRelease = onDropActionButton ProtocolGame.registerExtendedOpcode(116, onPociones) end --- Handles termination event function ActionBarController:onTerminate() ProtocolGame.unregisterExtendedOpcode(116) ApiJson.saveData() cleanupMultiActionState() for _, actionbar in pairs(actionBars) do if actionbar and not actionbar:isDestroyed() then actionbar:destroy() end end if mouseGrabberWidget then mouseGrabberWidget:destroy() mouseGrabberWidget = nil end if lastHighlightWidget then lastHighlightWidget:destroy() lastHighlightWidget = nil end if dragButton then dragButton:destroy() dragButton = nil end if dragItem then dragItem:destroy() dragItem = nil end actionBars = {} activeActionBars = {} disconnecting() end --- Handles game start event function ActionBarController:onGameStart() onCreateActionBars() -- Ensure fresh cache if clearHotkeyCache then clearHotkeyCache() end updateActionBarEventSubscriptions() dragItem = nil dragButton = nil cachedItemWidget = {} player = g_game.getLocalPlayer() -- OT-MMO: perfil de teclas por clase (Guardian = Knight, Druida = Druid, Hechicero = Sorcerer, Cazador = Paladin). local perfilPorVocacion = { [1] = "Knight", [11] = "Knight", [4] = "Druid", [14] = "Druid", [3] = "Sorcerer", [13] = "Sorcerer", [2] = "Paladin", [12] = "Paladin", [6] = "Brujo" } local perfil = player and perfilPorVocacion[player:getVocation()] if perfil and ApiJson and ApiJson.setCurrentHotkeySetName then ApiJson.setCurrentHotkeySetName(perfil) -- OT-MMO (23-oct): cada personaje guarda sus barras («Brujo:Kitbrujo»). La primera vez se copian del perfil de su -- clase (lo que ya tenía: nadie pierde su barra) y desde ahí son suyas. local propio = perfil .. ':' .. player:getName() if not ApiJson.setCurrentHotkeySetName(propio) and ApiJson.createHotkeySet(propio, perfil) then ApiJson.setCurrentHotkeySetName(propio) end teclasSegundaBarra() ApiJson.saveData() end hotkeyItemList = {} spellGroupPressed = {} spellGroupCooldownCache = {} for i = 1, #actionBars do setupActionBar(i) end -- OT-MMO (23-oct): la segunda barra sale para todos una vez; luego se oculta o muestra desde «Más» y se respeta. if not g_settings.getBoolean('otmmoDosBarras', false) then g_settings.set('otmmoDosBarras', true) if not segundaBarraVisible() then configureActionBar('actionBarShowBottom2', true) end end ActionBarController:scheduleEvent(function() onMultiUseCooldown() onUpdateActionBarStatus() updateActionPassive() updateVisibleWidgets() isLoaded = true end, 300, "update_items") end function ActionBarController:onGameEnd() ApiJson.saveData() isLoaded = false cleanupMultiActionState() spellGroupCooldownCache = {} for _, actionbar in pairs(activeActionBars) do unbindActionBarEvent(actionbar) end activeActionBars = {} hotkeyItemList = {} if g_tooltip then g_tooltip.hide() g_tooltip.hideSpecial() end updateActionBarEventSubscriptions() end -- /*============================================= -- = Events Call = -- =============================================*/ --- Handles spell cooldown events local pendingMultiButtonUpdate = nil local pendingMultiButtons = {} local pendingMultiButtonsTracker = {} local function schedulePendingMultiButtonUpdate() if pendingMultiButtonUpdate then return end pendingMultiButtonUpdate = scheduleEvent(function() for _, button in ipairs(pendingMultiButtons) do if button and not button:isDestroyed() then updateMultiButtonState(button) end end pendingMultiButtons = {} pendingMultiButtonsTracker = {} pendingMultiButtonUpdate = nil end, 50) end local function addPendingMultiButton(button) if not pendingMultiButtonsTracker[button] then pendingMultiButtons[#pendingMultiButtons + 1] = button pendingMultiButtonsTracker[button] = true end end function onSpellCooldown(spellId, delay) local showProgress = modules.client_options.getOption("graphicalCooldown") local showTime = modules.client_options.getOption("cooldownSecond") if not showProgress and not showTime then return true end local cooldownStart = g_clock.millis() local isRune = Spells.isRuneSpell(spellId) spellCooldownCache[spellId] = { exhaustion = delay, startTime = cooldownStart } for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do local cache = getButtonCache(button) if cache and cache.multiActions and not table.empty(cache.multiActions) then for _, actionData in pairs(cache.multiActions) do if actionData and actionData["chatText"] then local spellData = Spells.getSpellDataByParamWords(actionData["chatText"]:lower()) if spellData and spellData.id == spellId then addPendingMultiButton(button) if scheduleMultiActionCooldownEvent then local effectiveDelay = math.max(delay, spellData.exhaustion or 0) scheduleMultiActionCooldownEvent(button, "spell_" .. spellId, effectiveDelay) end break end elseif actionData and actionData["useObject"] then local runeSpellData = Spells.getRuneSpellByItem(actionData["useObject"]) if runeSpellData and runeSpellData.id == spellId then addPendingMultiButton(button) if scheduleMultiActionCooldownEvent then local effectiveDelay = math.max(delay, runeSpellData.exhaustion or 0) scheduleMultiActionCooldownEvent(button, "rune_" .. spellId, effectiveDelay) end break end end end end if cache and (cache.isSpell or cache.isRuneSpell) then local shouldUpdate = true if cache.isRuneSpell and not isRune then shouldUpdate = false elseif cache.isRuneSpell and (not cache.spellData or cache.spellData.id ~= spellId) then shouldUpdate = false elseif not cache.isRuneSpell and cache.spellID ~= spellId then shouldUpdate = false elseif cache.cooldownEvent ~= nil and button.cooldown:getTimeElapsed() > delay then shouldUpdate = false end if shouldUpdate then updateCooldown(button, delay) if cache.removeCooldownEvent then removeEvent(button.cache.removeCooldownEvent) button.cache.removeCooldownEvent = nil end button.cache.removeCooldownEvent = scheduleEvent(function() removeCooldown(button) end, delay) end end end end schedulePendingMultiButtonUpdate() end function onSpellGroupCooldown(groupId, delay) local showProgress = modules.client_options.getOption("graphicalCooldown") local showTime = modules.client_options.getOption("cooldownSecond") if not showProgress and not showTime then return true end spellGroupCooldownCache[groupId] = { exhaustion = delay, startTime = g_clock.millis() } for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do local cache = getButtonCache(button) if cache and cache.multiActions and not table.empty(cache.multiActions) then for _, actionData in pairs(cache.multiActions) do if actionData and actionData["chatText"] then local spellData = Spells.getSpellDataByParamWords(actionData["chatText"]:lower()) if spellData and (Spells.getCooldownByGroup(spellData, groupId) or Spells.getCooldownBySecondaryGroup(spellData, groupId)) then addPendingMultiButton(button) if scheduleMultiActionCooldownEvent then scheduleMultiActionCooldownEvent(button, "group_" .. groupId .. "_spell_" .. spellData.id, delay) end break end elseif actionData and actionData["useObject"] then local runeSpellData = Spells.getRuneSpellByItem(actionData["useObject"]) if runeSpellData and (Spells.getCooldownByGroup(runeSpellData, groupId) or Spells.getCooldownBySecondaryGroup(runeSpellData, groupId)) then addPendingMultiButton(button) if scheduleMultiActionCooldownEvent then scheduleMultiActionCooldownEvent(button, "group_" .. groupId .. "_rune_" .. runeSpellData.id, delay) end break end end end end if cache and (not cache.isRuneSpell and cache.spellData) then if Spells.getCooldownByGroup(cache.spellData, groupId) then local resttime = button.cooldown:getDuration() - button.cooldown:getTimeElapsed() if resttime < delay then updateCooldown(button, delay) if button.cache.removeCooldownEvent then removeEvent(button.cache.removeCooldownEvent) button.cache.removeCooldownEvent = nil end button.cache.removeCooldownEvent = scheduleEvent(function() removeCooldown(button) end, delay) end end if Spells.getCooldownBySecondaryGroup(cache.spellData, groupId) then local resttime = button.cooldown:getDuration() - button.cooldown:getTimeElapsed() if resttime < delay then updateCooldown(button, delay) if button.cache.removeCooldownEvent then removeEvent(button.cache.removeCooldownEvent) button.cache.removeCooldownEvent = nil end button.cache.removeCooldownEvent = scheduleEvent(function() removeCooldown(button) end, delay) end end end end end schedulePendingMultiButtonUpdate() end --- Handles passive ability data updates function onPassiveData(currentCooldown, maxCooldown, canDecay) passiveData = { cooldown = currentCooldown, max = maxCooldown } updateActionPassive() end --- Handles spell list changes function onSpellsChange(player, list) spellListData = {} for _, spellId in pairs(list) do local spell = Spells.getSpellByClientId(spellId) if spell then spellListData[tostring(spell.id)] = spell end end end function onHotkeyItems(itemList) for _, data in pairs(itemList) do table.insert(hotkeyItemList, data) end for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do if button.item and button.item:getItemId() >= 100 then setupButtonTooltip(button, false) end end end end --- Handles level updates function onUpdateLevel(localPlayer, level, levelPercent, oldLevel, oldLevelPercent) if level ~= oldLevel then onUpdateActionBarStatus() end end --- Handles multi-use cooldown updates -- OT-MMO: espera compartida de las pociones (server .../actions/items/potions.lua, opcode 116 «P|ms»): 60 s en combate, -- 10 s fuera. Solo se pinta en los botones de poción; el 1 s de «usar objeto» que manda Canary no la pisa. local finPociones = 0 local function esPocion(button) local item = button.item and button.cache and button.cache.itemId and button.item:getItem() return item and item:getMarketData() and item:getMarketData().category == MarketCategory.Potions end function onPociones(protocol, opcode, buffer) -- sin local: onInit (mas arriba) la registra local ms = tonumber(buffer:match('^P|(%d+)$') or '') if not ms then return end finPociones = g_clock.millis() + ms for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do if esPocion(button) then updateCooldown(button, ms) end end end end function onMultiUseCooldown(multiUseCooldown) for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do updateButtonState(button) if multiUseCooldown and button.item and button.cache and button.cache.itemId then local item = button.item:getItem() if item and item:isMultiUse() then local marketArray = {MarketCategory.Potions, MarketCategory.Runes, MarketCategory.Tools} if table.contains(marketArray, item:getMarketData().category) and not (item:getMarketData().category == MarketCategory.Potions and g_clock.millis() < finPociones) then updateCooldown(button, multiUseCooldown) end end end end end end function updateInventoryItems(_) for _, widgetList in pairs(cachedItemWidget) do for _, widget in pairs(widgetList) do updateButtonState(widget) end end end -- OT-MMO (23-oct): teclas de la segunda barra. Sin chat: Shift+1 ... Shift+0; con el chat activo: Shift+F1 ... Shift+F10 -- (como la principal, que usa 1-0 y F1-F10). Solo se ponen una vez por perfil y si la tecla está libre: si el -- jugador luego cambia o quita una (clic derecho > Cambiar tecla), se respeta. function teclasSegundaBarra() local nombre = ApiJson.getCurrentHotkeySetName and ApiJson.getCurrentHotkeySetName() local marca = 'otmmoTeclasBarra2_' .. tostring(nombre) if not nombre or g_settings.getBoolean(marca, false) then return end for _, modo in ipairs({ 'chatOff', 'chatOn' }) do local entradas = ApiJson.getHotkeyEntries(modo) local acciones, teclas = {}, {} for _, e in ipairs(entradas) do if e.actionsetting and e.actionsetting.action then acciones[e.actionsetting.action] = true end if e.keysequence and e.keysequence ~= '' then teclas[e.keysequence:lower()] = true end end for i = 1, 10 do local accion = 'TriggerActionButton_2.' .. i local tecla = modo == 'chatOff' and ('Shift+' .. (i % 10)) or ('Shift+F' .. i) if not acciones[accion] and not teclas[tecla:lower()] then entradas[#entradas + 1] = { actionsetting = { action = accion }, keysequence = tecla } end end end if clearHotkeyCache then clearHotkeyCache() end g_settings.set(marca, true) end function segundaBarraVisible() local estado = ApiJson.getActionBar(2) return estado and estado.isVisible and true or false end function alternarSegundaBarra() configureActionBar('actionBarShowBottom2', not segundaBarraVisible()) ApiJson.saveData() updateVisibleWidgets() end --- Updates visible widgets externally function updateVisibleWidgetsExternal() updateVisibleWidgets() end function selectHotkeySet(name) if not ApiJson or not ApiJson.setCurrentHotkeySetName then return false end if not ApiJson.setCurrentHotkeySetName(name) then return false end if clearHotkeyCache then clearHotkeyCache() end ApiJson.saveData() if #actionBars == 0 then return true end for _, actionbar in pairs(actionBars) do if actionbar and not actionbar:isDestroyed() then unbindActionBarEvent(actionbar) end end for i = 1, #actionBars do setupActionBar(i) end updateVisibleWidgets() updateActionBarEventSubscriptions() return true end function createHotkeySet(name, sourceName) if not ApiJson or not ApiJson.createHotkeySet then return false end local ok = ApiJson.createHotkeySet(name, sourceName) if ok then ApiJson.saveData() end return ok end function renameHotkeySet(oldName, newName) if not ApiJson or not ApiJson.renameHotkeySet then return false end local ok = ApiJson.renameHotkeySet(oldName, newName) if ok then ApiJson.saveData() end return ok end function removeHotkeySet(name) if not ApiJson or not ApiJson.removeHotkeySet then return false end local ok = ApiJson.removeHotkeySet(name) if ok then ApiJson.saveData() end return ok end --- Toggles cooldown display options function toggleCooldownOption() for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do if button.cooldown and button.cooldown:getPercent() < 100 then local remaining = button.cooldown:getDuration() - button.cooldown:getTimeElapsed() if remaining > 0 then updateCooldown(button, remaining) end end end end end function configureActionBar(key, value) local map = { actionBarShowBottom1 = 1, actionBarShowBottom2 = 2, actionBarShowBottom3 = 3, actionBarShowLeft1 = 4, actionBarShowLeft2 = 5, actionBarShowLeft3 = 6, actionBarShowRight1 = 7, actionBarShowRight2 = 8, actionBarShowRight3 = 9 } local n = map[key] if not n then return end local actionbar = actionBars[n] if actionbar then ApiJson.setBarVisibility(n, key, value) actionbar:setVisible(value) actionbar:setOn(value) if value then addActiveActionBar(actionbar) setupActionBar(n) else removeActiveActionBar(actionbar) end if resizeLockButtons then resizeLockButtons() end if ActionBarController then ActionBarController:scheduleEvent(onUpdateActionBarStatus) end end end --- Updates visible options function updateVisibleOptions(type, value) for _, actionbar in pairs(activeActionBars) do for _, button in pairs(actionbar.tabBar:getChildren()) do if type == 'hotkey' and button.hotkeyLabel then button.hotkeyLabel:setVisible(value) elseif type == 'parameter' and button.parameterText then button.parameterText:setVisible(value) elseif type == 'tooltip' then setupButtonTooltip(button, false) elseif type == 'amount' then updateButtonState(button) end end end end --- Resets a specific action bar function resetAction(barId) local actionbar = actionBars[barId] if not actionbar then return end for i = 1, 50 do ApiJson.removeAction(barId, i) local button = actionbar.tabBar:getChildById(barId .. "." .. i) if button then updateButton(button) end end end --- Resets all action bars function resetActionBars() for i = 1, 9 do resetAction(i) end end