-- Add capitalize function to string library if it doesn't exist if not string.capitalize then function string.capitalize(str) if not str or str == "" or str == nil then return "Unknown" end return str:gsub("(%l)(%w*)", function(first, rest) return first:upper() .. rest end) end end if not BossCooldown then BossCooldown = { launchTime = 0, lastTick = 0, sort = 0, search = "", cooldown = {}, widgets = {}, window = nil, } BossCooldown.__index = BossCooldown end local widgets = {} function BossCooldown.create() BossCooldown.launchTime = 0 BossCooldown.sort = 0 BossCooldown.lastTick = 0 BossCooldown.search = "" BossCooldown.cooldown = {} BossCooldown.widgets = {} BossCooldown.window = openedWindows["bossButton"] if not BossCooldown.window then return end local toggleFilterButton = BossCooldown.window:recursiveGetChildById("toggleFilterButton") if toggleFilterButton then toggleFilterButton:setVisible(false) end local newWindowButton = BossCooldown.window:recursiveGetChildById("newWindowButton") if newWindowButton then newWindowButton:setVisible(false) end local contextMenuButton = BossCooldown.window:recursiveGetChildById("contextMenuButton") local minimizeButton = BossCooldown.window:recursiveGetChildById("minimizeButton") if contextMenuButton and minimizeButton then contextMenuButton:setVisible(true) contextMenuButton:breakAnchors() contextMenuButton:addAnchor(AnchorTop, minimizeButton:getId(), AnchorTop) contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft) contextMenuButton:setMarginRight(7) contextMenuButton:setMarginTop(0) contextMenuButton.onClick = function(widget, mousePos) local pos = mousePos or g_window.getMousePosition() return onBossExtra(pos) end end local lockButton = BossCooldown.window:recursiveGetChildById("lockButton") if lockButton and contextMenuButton then lockButton:setVisible(true) lockButton:breakAnchors() lockButton:addAnchor(AnchorTop, contextMenuButton:getId(), AnchorTop) lockButton:addAnchor(AnchorRight, contextMenuButton:getId(), AnchorLeft) lockButton:setMarginRight(2) lockButton:setMarginTop(0) end local cl = openedWindows["bossButton"]:recursiveGetChildById("clickablePanel") if cl then cl.onMouseWheel = scrollUIPanel end -- Set up the event handlers for the search text local searchText = BossCooldown.window.contentsPanel.searchText if searchText then -- Store original handlers BossCooldown.originalOnKeyPress = searchText.onKeyPress BossCooldown.originalOnTextChange = searchText.onTextChange -- Override onKeyPress to unbind movement keys immediately when typing searchText.onKeyPress = function(widget, keyCode, keyboardModifiers) -- Unbind movement keys when user starts typing local gameWalk = modules.game_walk if gameWalk then gameWalk.unbindWalkKey("W") gameWalk.unbindWalkKey("D") gameWalk.unbindWalkKey("S") gameWalk.unbindWalkKey("A") gameWalk.unbindWalkKey("E") gameWalk.unbindWalkKey("Q") gameWalk.unbindWalkKey("C") gameWalk.unbindWalkKey("Z") gameWalk.unbindTurnKey("Control+W") gameWalk.unbindTurnKey("Control+D") gameWalk.unbindTurnKey("Control+S") gameWalk.unbindTurnKey("Control+A") end -- Handle Escape key to clear focus and restore movement if keyCode == KeyEscape then -- Re-bind movement keys local gameWalk = modules.game_walk if gameWalk then gameWalk.bindWalkKey("W", North) gameWalk.bindWalkKey("D", East) gameWalk.bindWalkKey("S", South) gameWalk.bindWalkKey("A", West) gameWalk.bindWalkKey("E", NorthEast) gameWalk.bindWalkKey("Q", NorthWest) gameWalk.bindWalkKey("C", SouthEast) gameWalk.bindWalkKey("Z", SouthWest) gameWalk.bindTurnKey("Control+W", North) gameWalk.bindTurnKey("Control+D", East) gameWalk.bindTurnKey("Control+S", South) gameWalk.bindTurnKey("Control+A", West) end widget:clearFocus() return false end -- Call original handler if it exists if BossCooldown.originalOnKeyPress then return BossCooldown.originalOnKeyPress(widget, keyCode, keyboardModifiers) end return false end -- Set up focus change handler searchText.onFocusChange = onBossSearchFocusChange end end function BossCooldown:reset() BossCooldown.launchTime = g_clock.millis() BossCooldown.sort = 0 BossCooldown.lastTick = 0 BossCooldown.search = "" BossCooldown.cooldown = {} BossCooldown.widgets = {} BossCooldown:updateWindow() end function BossCooldown:checkTicks() if self.lastTick - os.time() > 0 then return end self.lastTick = os.time() + 1 local layout = self.window.contentsPanel.bosses:getLayout() local needUpdate = false for _, widget in ipairs(self.widgets) do layout:enableUpdates() if self.search == "" or string.find(widget.name:lower(), self.search:lower(), 1, true) then widget:setVisible(true) else widget:setVisible(false) end layout:disableUpdates() widget.tick = widget.tick - 1 widget.cooldown = os.time() + widget.tick if widget.cooldown < os.time() then widget.cooldown = os.time() + 60 * 365 * 60 * 24 end local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown") if bossCooldownLabel then if widget.tick <= 0 then bossCooldownLabel:setText("No Cooldown") bossCooldownLabel:setColor("#c0c0c0") if widget.type ~= "nocd" then needUpdate = true widget.cooldown = os.time() + 60 * 365 * 60 * 24 end widget.type = "nocd" elseif widget.tick <= 60 then bossCooldownLabel:setText(widget.tick .. "s") bossCooldownLabel:setColor("#ff9854") if widget.type ~= "second" then needUpdate = true end widget.type = "second" else local duration = math.max(1, widget.tick) local days = math.floor(duration / 86400) local hours = math.floor((duration % 86400) / 3600) local minutes = math.floor((duration % 3600) / 60) if days > 0 then bossCooldownLabel:setText(string.format("%dd %02dh %02dmin", days, hours, minutes)) else bossCooldownLabel:setText(string.format("%02dh %02dmin", hours, minutes)) end bossCooldownLabel:setColor("#ff9854") if widget.type ~= "timed" then needUpdate = true end widget.type = "timed" end end end layout:enableUpdates() if needUpdate then if self.sort == 0 then table.sort(self.widgets, function(a, b) return a.cooldown < b.cooldown end) else table.sort(self.widgets, function(a, b) return a.name < b.name end) end self.window.contentsPanel.bosses:reorderChildren(self.widgets) end end function BossCooldown:updateWindow() local contentsPanel = BossCooldown.window.contentsPanel BossCooldown.widgets = {} contentsPanel.bosses:destroyChildren() if BossCooldown.sort == 0 then table.sort(BossCooldown.cooldown, function(a, b) local acd = a.cooldown if acd < os.time() then acd = os.time() + 60 * 365 * 60 * 24 end local bcd = b.cooldown if bcd < os.time() then bcd = os.time() + 60 * 365 * 60 * 24 end return acd < bcd end) else table.sort(BossCooldown.cooldown, function(a, b) return a.name < b.name end) end contentsPanel.searchText:setText("", false) local c = 1 for _, info in ipairs(BossCooldown.cooldown) do local widget = g_ui.createWidget("BossInfo", contentsPanel.bosses) local creatureWidget = widget:recursiveGetChildById("creature") local bossNameLabel = widget:recursiveGetChildById("bossName") local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown") if creatureWidget then if info.outfit then creatureWidget:setOutfit(info.outfit) else local fallbackOutfit = { type = 130, head = 0, body = 0, legs = 0, feet = 0, addons = 0, } creatureWidget:setOutfit(fallbackOutfit) end end if bossNameLabel then local bossName = info.name if not bossName or bossName == "" or bossName:trim() == "" then bossName = "Boss " .. (info.bossId or "Unknown") end local displayName = short_text(string.capitalize(bossName), 13) bossNameLabel:setText(displayName) widget:setTooltip(string.capitalize(bossName)) else widget:setTooltip("Unknown Boss") end widget.onClick = function() if modules.game_cyclopedia then modules.game_cyclopedia.show("bosstiary") end end local resttime = math.max(0, info.cooldown - os.time()) if bossCooldownLabel then if resttime <= 0 then bossCooldownLabel:setText("No Cooldown") bossCooldownLabel:setColor("#c0c0c0") widget.type = "nocd" elseif resttime <= 60 then bossCooldownLabel:setText(resttime .. "s") bossCooldownLabel:setColor("#ff9854") widget.type = "second" else local duration = math.max(1, resttime) local days = math.floor(duration / 86400) local hours = math.floor((duration % 86400) / 3600) local minutes = math.floor((duration % 3600) / 60) if days > 0 then bossCooldownLabel:setText(string.format("%dd %02dh %02dmin", days, hours, minutes)) else bossCooldownLabel:setText(string.format("%02dh %02dmin", hours, minutes)) end bossCooldownLabel:setColor("#ff9854") widget.type = "timed" end end -- TODO: Implement tracker cooldown functionality when game_trackers module is available widget.tick = resttime widget.name = info.name widget.bossId = info.bossId c = c + 1 BossCooldown.widgets[#BossCooldown.widgets + 1] = widget end contentsPanel.bosses:setHeight(40 + (35 * c)) end function BossCooldown:setupCooldown(cooldown) BossCooldown.cooldown = {} for i, cooldownData in pairs(cooldown) do local raceData = g_things.getRaceData(cooldownData.bossRaceId) local bossEntry = { bossId = cooldownData.bossRaceId, cooldown = cooldownData.cooldownTime, name = raceData and raceData.name or "", outfit = raceData and raceData.outfit or nil, } BossCooldown.cooldown[#BossCooldown.cooldown + 1] = bossEntry end BossCooldown.widgets = {} BossCooldown:updateWindow() end function checkBossSearch(text) if not text or #text <= 1 then BossCooldown.search = "" else BossCooldown.search = text end -- Immediately apply the search filter if BossCooldown.window and BossCooldown.window.contentsPanel and BossCooldown.window.contentsPanel.bosses then local layout = BossCooldown.window.contentsPanel.bosses:getLayout() if layout then layout:enableUpdates() for _, widget in ipairs(BossCooldown.widgets) do if BossCooldown.search == "" or string.find(widget.name:lower(), BossCooldown.search:lower(), 1, true) then widget:setVisible(true) else widget:setVisible(false) end end layout:disableUpdates() layout:update() end end end function onBossSearchFocusChange(widget, focused) if focused then -- When gaining focus, unbind movement keys local gameWalk = modules.game_walk if gameWalk then gameWalk.unbindWalkKey("W") gameWalk.unbindWalkKey("D") gameWalk.unbindWalkKey("S") gameWalk.unbindWalkKey("A") gameWalk.unbindWalkKey("E") gameWalk.unbindWalkKey("Q") gameWalk.unbindWalkKey("C") gameWalk.unbindWalkKey("Z") gameWalk.unbindTurnKey("Control+W") gameWalk.unbindTurnKey("Control+D") gameWalk.unbindTurnKey("Control+S") gameWalk.unbindTurnKey("Control+A") end else -- When losing focus, bind movement keys back local gameWalk = modules.game_walk if gameWalk then gameWalk.bindWalkKey("W", North) gameWalk.bindWalkKey("D", East) gameWalk.bindWalkKey("S", South) gameWalk.bindWalkKey("A", West) gameWalk.bindWalkKey("E", NorthEast) gameWalk.bindWalkKey("Q", NorthWest) gameWalk.bindWalkKey("C", SouthEast) gameWalk.bindWalkKey("Z", SouthWest) gameWalk.bindTurnKey("Control+W", North) gameWalk.bindTurnKey("Control+D", East) gameWalk.bindTurnKey("Control+S", South) gameWalk.bindTurnKey("Control+A", West) end end end function clearSearch() BossCooldown.search = "" BossCooldown.window.contentsPanel.searchText:setText("", false) end function onBossExtra(mousePosition) if cancelNextRelease then cancelNextRelease = false return false end local player = g_game.getLocalPlayer() if not player then return false end local sortByCooldown = BossCooldown.sort == 0 local sortByName = BossCooldown.sort == 1 local menu = g_ui.createWidget("PopupMenu") menu:setGameMenu(true) menu:addCheckBox(tr("sort by cooldown"), sortByCooldown, function() BossCooldown.sort = 0 BossCooldown:updateWindow() end) menu:addCheckBox(tr("sort by name"), sortByName, function() BossCooldown.sort = 1 BossCooldown:updateWindow() end) menu:display(mousePosition) return true end function toggleBossCDFocus(visible) local widget = BossCooldown.window:recursiveGetChildById("clickablePanel") if widget and visible then widget:setPhantom(true) elseif widget then widget:setPhantom(false) widget.onClick = function() modules.game_interface.toggleInternalFocus() toggleBossCDFocus(not visible) end end modules.game_interface.toggleFocus(visible, "bosscooldown") if visible then local text = BossCooldown.window.contentsPanel.searchText text:focus() -- Immediately unbind movement keys when search becomes active local gameWalk = modules.game_walk if gameWalk then gameWalk.unbindWalkKey("W") gameWalk.unbindWalkKey("D") gameWalk.unbindWalkKey("S") gameWalk.unbindWalkKey("A") gameWalk.unbindWalkKey("E") gameWalk.unbindWalkKey("Q") gameWalk.unbindWalkKey("C") gameWalk.unbindWalkKey("Z") gameWalk.unbindTurnKey("Control+W") gameWalk.unbindTurnKey("Control+D") gameWalk.unbindTurnKey("Control+S") gameWalk.unbindTurnKey("Control+A") end else -- Re-bind movement keys when search becomes inactive local gameWalk = modules.game_walk if gameWalk then gameWalk.bindWalkKey("W", North) gameWalk.bindWalkKey("D", East) gameWalk.bindWalkKey("S", South) gameWalk.bindWalkKey("A", West) gameWalk.bindWalkKey("E", NorthEast) gameWalk.bindWalkKey("Q", NorthWest) gameWalk.bindWalkKey("C", SouthEast) gameWalk.bindWalkKey("Z", SouthWest) gameWalk.bindTurnKey("Control+W", North) gameWalk.bindTurnKey("Control+D", East) gameWalk.bindTurnKey("Control+S", South) gameWalk.bindTurnKey("Control+A", West) end BossCooldown.window:setBorderWidth(0) end end function updateBossFocus() scheduleEvent(function() if not BossCooldown.window then return end local scrollBar = BossCooldown.window:recursiveGetChildById("miniwindowScrollBar") if not scrollBar then return end scrollBar:setValue(1) end, 1) end function BossCooldown:hasCooldown(raceId) for _, info in ipairs(BossCooldown.cooldown) do if info.bossId == raceId then return info.name, info.cooldown end end return "", -1 end function BossCooldown:getCooldown(raceId) for _, widget in pairs(BossCooldown.widgets) do if widget.bossId and widget.bossId == raceId and widget.cooldown and widget.cooldown > os.time() then local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown") if bossCooldownLabel then return bossCooldownLabel:getText() end end end return "" end