--- ============================ HEADER ============================ --- ======= LOCALIZE ======= -- Addon local addonName, HR = ... -- HeroLib local HL = HeroLib local Cache, Utils = HeroCache, HL.Utils local Unit = HL.Unit local Player = Unit.Player local Target = Unit.Target local Spell = HL.Spell local Item = HL.Item -- Lua local mathmin = math.min local print = print local select = select local stringlower = string.lower local strsplit = strsplit local tostring = tostring local GetTime = GetTime -- File Locals --- ======= GLOBALIZE ======= -- Addon HeroRotation = HR --- ============================ CONTENT ============================ --- ======= CORE ======= -- Print with ER Prefix function HR.Print(...) print("[|cFFFF6600Hero Rotation|r]", ...) end -- Defines the APL HR.APLs = {} HR.APLInits = {} function HR.SetAPL(Spec, APL, APLInit) HR.APLs[Spec] = APL HR.APLInits[Spec] = APLInit end -- Get the texture (and cache it until next reload). -- TODO: Implements GetTexture as Actions method (Item:GetTexture() / Spell:GetTexture() / Macro:GetTexture()) -- So we can simplify this part. function HR.GetTexture(Object) -- Spells local SpellID = Object.SpellID if SpellID then local TextureCache = Cache.Persistent.Texture.Spell if not TextureCache[SpellID] then -- Check if the SpellID is the one from Custom Textures or a Regular WoW Spell if SpellID >= 999900 then TextureCache[SpellID] = "Interface\\Addons\\HeroRotation\\Textures\\"..tostring(SpellID) elseif Object.TextureSpellID then TextureCache[SpellID] = GetSpellTexture(Object.TextureSpellID) else TextureCache[SpellID] = GetSpellTexture(SpellID) end end return TextureCache[SpellID] end -- Items local ItemID = Object.ItemID if ItemID then local TextureCache = Cache.Persistent.Texture.Item if not TextureCache[ItemID] then -- name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, vendorPrice local _, _, _, _, _, _, _, _, _, texture = GetItemInfo(ItemID) TextureCache[ItemID] = texture end return TextureCache[ItemID] end end --- ======= CASTS ======= local GCDSpell = Spell(61304) local CooldownSpell, CooldownSpellDisplayTime, CooldownSpellCastDuration local function DisplayCooldown(Object, DisplayPoolingSwirl, CustomTime) local StartTime, CastDuration -- Default GCD and Casting Swirls local CurrentTime = GetTime() if Player:IsCasting() or Player:IsChanneling() then StartTime = Player:CastStart() CastDuration = Player:CastDuration() else StartTime, CastDuration = GCDSpell:CooldownInfo() end -- Tracking Values for Current Spell if CooldownSpell ~= Object then CooldownSpell = Object CooldownSpellDisplayTime = CurrentTime CooldownSpellCastDuration = 0 end -- Resource Pooling Display Swirls if DisplayPoolingSwirl then local TimeToResource if CustomTime then TimeToResource = CustomTime else local Resource = Object:CostInfo(nil, "type") if Resource then TimeToResource = Player.TimeToXResourceMap[Resource](Object:Cost()) end end if TimeToResource and TimeToResource > 0 then -- Only display the resource-based swirl if the duration is greater than the GCD/Cast swirl if TimeToResource > ((StartTime + CastDuration) - CurrentTime) then local AdjustedCastDuration = CurrentTime - CooldownSpellDisplayTime + TimeToResource -- 0.25s minimum, don't display an increase unless it is greater than 0.5s if (CooldownSpellCastDuration == 0 and AdjustedCastDuration > 0.25) or CooldownSpellCastDuration > AdjustedCastDuration or (AdjustedCastDuration - CooldownSpellCastDuration) > 0.5 then CooldownSpellCastDuration = AdjustedCastDuration end StartTime = CooldownSpellDisplayTime CastDuration = CooldownSpellCastDuration end end end -- Reset tracking if the current cooldown is finished if((StartTime + CastDuration) < CurrentTime) then StartTime = 0 CastDuration = 0 CooldownSpell = nil end HR.MainIconFrame:SetCooldown(StartTime, CastDuration) end -- Flash Icon (SpellFlashCore support) local LastFlash = 0 local function FlashIcon(Object) if IsAddOnLoaded("SpellFlashCore") then local SpellID = Object.SpellID local ItemID = Object.ItemID local FlashInterval = 0.5 if SpellID and GetTime() - LastFlash > FlashInterval then SpellFlashCore.FlashAction(SpellID) LastFlash = GetTime() elseif ItemID and GetTime() - LastFlash > FlashInterval then SpellFlashCore.FlashItem(ItemID) LastFlash = GetTime() end end end -- Main Cast HR.CastOffGCDOffset = 1 function HR.Cast(Object, OffGCD, DisplayStyle, OutofRange, CustomTime) local ObjectTexture = HR.GetTexture(Object) local Keybind = not HR.GUISettings.General.HideKeyBinds and HL.Action.TextureHotKey(ObjectTexture) FlashIcon(Object) if OffGCD or DisplayStyle == "Cooldown" then -- If this is the second cooldown, check to ensure we don't have a duplicate icon in the first slot if HR.CastOffGCDOffset == 1 or (HR.CastOffGCDOffset == 2 and HR.SmallIconFrame:GetIcon(1) ~= ObjectTexture) then HR.SmallIconFrame:ChangeIcon(HR.CastOffGCDOffset, ObjectTexture, Keybind, OutofRange) HR.CastOffGCDOffset = HR.CastOffGCDOffset + 1 Object.LastDisplayTime = GetTime() return false end elseif DisplayStyle == "Suggested" then HR.CastSuggested(Object, OutofRange) elseif DisplayStyle == "SuggestedRight" then HR.CastRightSuggested(Object, OutofRange) else local PoolResource = 999910 local Usable = Object.SpellID == PoolResource or Object:IsUsable() local ShowPooling = DisplayStyle == "Pooling" local OutofRange = OutofRange or false HR.MainIconFrame:ChangeIcon(ObjectTexture, Keybind, Usable, OutofRange, Object:ID()) DisplayCooldown(Object, ShowPooling, CustomTime) Object.LastDisplayTime = GetTime() return true end return nil end -- Overload for Main Cast (with text) function HR.CastAnnotated(Object, OffGCD, Text, OutofRange, FontSize) local Result = HR.Cast(Object, OffGCD, nil, OutofRange) -- TODO: handle small icon frame if OffGCD is true if not OffGCD then HR.MainIconFrame:OverlayText(Text, FontSize) end return Result end -- Overload for Main Cast (with resource pooling swirl) function HR.CastPooling(Object, CustomTime, OutofRange) return HR.Cast(Object, false, "Pooling", OutofRange, CustomTime) end -- Queued Casting Support local QueueSpellTable, QueueLength, QueueTextureTable, QueueKeybindTable HR.MaxQueuedCasts = 3 local function DisplayQueue(...) QueueSpellTable = {...} QueueLength = mathmin(#QueueSpellTable, HR.MaxQueuedCasts) QueueTextureTable = {} QueueKeybindTable = {} for i = 1, QueueLength do QueueTextureTable[i] = HR.GetTexture(QueueSpellTable[i]) QueueSpellTable[i].LastDisplayTime = GetTime() QueueKeybindTable[i] = not HR.GUISettings.General.HideKeyBinds and HL.Action.TextureHotKey(QueueTextureTable[i]) end -- Call ChangeIcon so that the main icon exists to be able to display a cooldown sweep, even though it gets overlapped FlashIcon(QueueSpellTable[1]) HR.MainIconFrame:ChangeIcon(QueueTextureTable[1], QueueKeybindTable[1], QueueSpellTable[1]:IsUsable(), false, QueueSpellTable[1]:ID()) HR.MainIconFrame:SetupParts(QueueTextureTable, QueueKeybindTable) end -- Main Cast Queue function HR.CastQueue(...) DisplayQueue(...) DisplayCooldown() return "Should Return" end -- Pooling Cast Queue function HR.CastQueuePooling(CustomTime, ...) DisplayQueue(...) -- If there is a custom time, just pass in the first spell if CustomTime then DisplayCooldown(QueueSpellTable[1], true, CustomTime) else -- Find the largest cost in the table to use as the cooldown object local CostObject, MaxCost = nil, 0 for i = 1, #QueueSpellTable do if QueueSpellTable[i]:Cost() > MaxCost then MaxCost = QueueSpellTable[i]:Cost() CostObject = QueueSpellTable[i] end end DisplayCooldown(CostObject, true) end return "Should Return" end -- Left (+ Nameplate) Cast HR.CastLeftOffset = 1 function HR.CastLeftCommon(Object) local Texture = HR.GetTexture(Object) local Keybind = not HR.GUISettings.General.HideKeyBinds and HL.Action.TextureHotKey(Texture) FlashIcon(Object) HR.LeftIconFrame:ChangeIcon(Texture, Keybind) HR.CastLeftOffset = HR.CastLeftOffset + 1 Object.LastDisplayTime = GetTime() end function HR.CastLeft(Object) if HR.CastLeftOffset == 1 then HR.CastLeftCommon(Object) end return false end function HR.CastLeftNameplate(ThisUnit, Object) if HR.CastLeftOffset == 1 and HR.Nameplate.AddIcon(ThisUnit, Object) then HR.CastLeftCommon(Object) end return false end -- Used by experimental protection paladin module function HR.CastMainNameplate(ThisUnit, Object) if HR.Nameplate.AddIcon(ThisUnit, Object) then return HR.Cast(Object) end return false end function HR.CastMainNameplateSuggested(ThisUnit, Object) if HR.Nameplate.AddSuggestedIcon(ThisUnit, Object) then return HR.CastRightSuggested(Object) end return false end -- Suggested Icon Cast HR.CastSuggestedOffset = 1 function HR.CastSuggested(Object, OutofRange) if HR.CastSuggestedOffset == 1 then local Texture = HR.GetTexture(Object) local Keybind = not HR.GUISettings.General.HideKeyBinds and HL.Action.TextureHotKey(Texture) FlashIcon(Object) HR.SuggestedIconFrame:ChangeIcon(Texture, Keybind, OutofRange, Object:ID()) HR.CastSuggestedOffset = HR.CastSuggestedOffset + 1 Object.LastDisplayTime = GetTime() end return false end -- Suggested Icon (Right) Cast HR.CastRightSuggestedOffset = 1 function HR.CastRightSuggested(Object, OutofRange) if HR.CastRightSuggestedOffset == 1 then local Texture = HR.GetTexture(Object) local Keybind = not HR.GUISettings.General.HideKeyBinds and HL.Action.TextureHotKey(Texture) FlashIcon(Object) HR.RightSuggestedIconFrame:ChangeIcon(Texture, Keybind, OutofRange, Object:ID()) HR.CastRightSuggestedOffset = HR.CastRightSuggestedOffset + 1 Object.LastDisplayTime = GetTime() end return false end --- ======= COMMANDS ======= -- Command Handler function HR.CmdHandler(Message) local Argument1, Argument2, Argument3 = strsplit(" ", stringlower(Message)) if Argument1 == "cds" then HeroRotationCharDB.Toggles[1] = not HeroRotationCharDB.Toggles[1] HR.ToggleIconFrame:UpdateButtonText(1) HR.Print("CDs are now "..(HeroRotationCharDB.Toggles[1] and "|cff00ff00enabled|r." or "|cffff0000disabled|r.")) elseif Argument1 == "aoe" then HeroRotationCharDB.Toggles[2] = not HeroRotationCharDB.Toggles[2] HR.ToggleIconFrame:UpdateButtonText(2) HR.Print("AoE is now "..(HeroRotationCharDB.Toggles[2] and "|cff00ff00enabled|r." or "|cffff0000disabled|r.")) elseif Argument1 == "toggle" then HeroRotationCharDB.Toggles[3] = not HeroRotationCharDB.Toggles[3] HR.ToggleIconFrame:UpdateButtonText(3) HR.Print("HeroRotation is now "..(HeroRotationCharDB.Toggles[3] and "|cff00ff00enabled|r." or "|cffff0000disabled|r.")) elseif Argument1 == "unlock" then HR.MainFrame:Unlock() HR.Print("HeroRotation UI is now |cff00ff00unlocked|r.") elseif Argument1 == "lock" then HR.MainFrame:Lock() HR.Print("HeroRotation UI is now |cffff0000locked|r.") elseif Argument1 == "scale" then if Argument2 and Argument3 then Argument3 = tonumber(Argument3) if Argument3 and type(Argument3) == "number" and Argument3 > 0 and Argument3 <= 10 then if Argument2 == "ui" then HR.MainFrame:ResizeUI(Argument3) elseif Argument2 == "buttons" then HR.MainFrame:ResizeButtons(Argument3) elseif Argument2 == "all" then HR.MainFrame:ResizeUI(Argument3) HR.MainFrame:ResizeButtons(Argument3) else HR.Print("Invalid |cff88ff88[Type]|r for Scale.") HR.Print("Should be |cff8888ff/hr scale|r |cff88ff88[Type]|r |cffff8888[Size]|r.") HR.Print("Type accepted are |cff88ff88ui|r, |cff88ff88buttons|r, |cff88ff88all|r.") end else HR.Print("Invalid |cffff8888[Size]|r for Scale.") HR.Print("Should be |cff8888ff/hr scale|r |cff88ff88[Type]|r |cffff8888[Size]|r.") HR.Print("Size accepted are |cffff8888number > 0 and <= 10|r.") end else HR.Print("Invalid arguments for Scale.") HR.Print("Should be |cff8888ff/hr scale|r |cff88ff88[Type]|r |cffff8888[Size]|r.") HR.Print("Type accepted are |cff88ff88ui|r, |cff88ff88buttons|r, |cff88ff88all|r.") HR.Print("Size accepted are |cffff8888number > 0 and <= 10|r.") end elseif Argument1 == "resetbuttons" then HR.ToggleIconFrame:ResetAnchor() elseif Argument1 == "debug" then HeroRotationCharDB.Toggles[4] = not HeroRotationCharDB.Toggles[4] HR.Print("Debug Output is now " .. (HeroRotationCharDB.Toggles[4] == true and "|cff00ff00enabled|r." or "|cffff0000disabled|r.")) elseif Argument1 == "flash" then HeroRotationCharDB.Toggles[5] = not HeroRotationCharDB.Toggles[5] HR.Print("Icon Flashing is now " .. (HeroRotationCharDB.Toggles[5] == true and "|cff00ff00enabled|r." or "|cffff0000disabled|r.")) elseif Argument1 == "help" then HR.Print("|cffffff00--[Toggles]--|r") HR.Print(" On/Off: |cff8888ff/hr toggle|r") HR.Print(" CDs: |cff8888ff/hr cds|r") HR.Print(" AoE: |cff8888ff/hr aoe|r") HR.Print(" Debug: |cff8888ff/hr debug|r") HR.Print(" Flash: |cff8888ff/hr flash|r") HR.Print("|cffffff00--[User Interface]--|r") HR.Print(" UI Lock: |cff8888ff/hr lock|r") HR.Print(" UI Unlock: |cff8888ff/hr unlock|r") HR.Print(" UI Scale: |cff8888ff/hr scale|r |cff88ff88[Type]|r |cffff8888[Size]|r") HR.Print(" [Type]: |cff88ff88ui|r, |cff88ff88buttons|r, |cff88ff88all|r") HR.Print(" [Size]: |cffff8888number > 0 and <= 10|r") HR.Print(" Button Anchor Reset : |cff8888ff/hr resetbuttons|r") else HR.Print("Invalid arguments.") HR.Print("Type |cff8888ff/hr help|r for more infos.") end end SLASH_HEROROTATION1 = "/hr" SLASH_HEROROTATION2 = "/ar" SlashCmdList["HEROROTATION"] = HR.CmdHandler -- Get if the CDs are enabled. function HR.CDsON() return HeroRotationCharDB.Toggles[1] end -- Check if debug is enabled function HR.DebugON() return HeroRotationCharDB.Toggles[4] end -- Check if icon flashing is enabled function HR.FlashON() return HeroRotationCharDB.Toggles[5] end -- Get if the AoE is enabled. do local AoEImmuneNPCID = { --- Legion ----- Dungeons (7.0 Patch) ----- --- Mythic+ Affixes -- Fel Explosives (7.2 Patch) [120651] = true } -- Disable the AoE if we target an unit that is immune to AoE spells. function HR.AoEON() return HeroRotationCharDB.Toggles[2] and not AoEImmuneNPCID[Target:NPCID()] end end -- Get if the main toggle is on. function HR.ON() return HeroRotationCharDB.Toggles[3] end -- Get if the UI is locked. function HR.Locked() return HeroRotationDB.Locked end