local Auras = {} local stealableColor = {r = 1, g = 1, b = 1} local playerUnits = {player = true, vehicle = true, pet = true} -- CoA: UnitAura "HARMFUL|RAID" only honours vanilla class dispels; custom classes -- need a manual type check. playerCoaDispels is nil until first checked, false if -- the player is not a dispelling CoA class, or a {type=true} set if they are. local playerCoaDispels -- Class tokens: MONK = Templar, PROPHET = Venomancer, WILDWALKER = Primalist -- (CoA in-game display names differ from internal class tokens). local COA_CLASS_DISPELS = { ["CHRONOMANCER"] = { Magic = true, Curse = true, Disease = true, Poison = true }, ["MONK"] = { Magic = true, Disease = true, Poison = true }, -- Templar (Rebuke) ["PROPHET"] = { Poison = true }, -- Venomancer (Antivenom) ["PYROMANCER"] = { Disease = true, Poison = true }, ["RANGER"] = { Disease = true, Poison = true }, ["CULTIST"] = { Curse = true }, ["SONOFARUGAL"] = { Curse = true }, ["SPIRITMAGE"] = { Magic = true }, ["STARCALLER"] = { Magic = true }, ["WITCHHUNTER"] = { Curse = true }, ["SUNCLERIC"] = { Magic = true, Disease = true, Poison = true }, -- Sanctify ["WILDWALKER"] = { Disease = true, Poison = true }, -- Primalist (Soothing Touch — DBC says Magic, gameplay is Poison/Disease) ["WITCHDOCTOR"] = { Curse = true, Disease = true, Poison = true }, -- Hexbreak (806240, single-target Curse) + Cleansing Idol (504840, AoE Disease/Poison) ["TINKER"] = { Disease = true, Poison = true }, -- Nanobot Cleanser } local function getCoaDispels() if playerCoaDispels ~= nil then return playerCoaDispels end -- Trust the COA_CLASS_DISPELS table directly: it only contains custom-class -- tokens, so any token-match implies the player IS a dispelling custom -- class. Avoids C_Player.IsCustomClass timing/availability issues (the -- API isn't reliable for every class on every login — Witchdoctor in -- particular was getting cached as false on raid frames). local _, token = UnitClass("player") if not token or token == "" then -- UnitClass not ready yet (very early init); don't cache — retry on -- the next scan so we pick it up once in-world. return nil end playerCoaDispels = COA_CLASS_DISPELS[token] or false return playerCoaDispels end -- Expose for other modules (highlight.lua) so the dispel set stays single-source. ShadowUF.GetCoaDispels = getCoaDispels local mainHand, offHand = {time = 0}, {time = 0} local tempEnchantScan ShadowUF:RegisterModule(Auras, "auras", ShadowUF.L["Auras"]) function Auras:OnEnable(frame) frame.auras = frame.auras or {} frame:RegisterNormalEvent("PLAYER_ENTERING_WORLD", self, "Update") frame:RegisterUnitEvent("UNIT_AURA", self, "Update") frame:RegisterUpdateFunc(self, "Update") self:UpdateFilter(frame) end function Auras:OnDisable(frame) frame:UnregisterAll(self) end -- Aura positioning code -- Definitely some of the more unusual code I've done, not sure I really like this method -- but it does allow more flexibility with how things are anchored without me having to hardcode the 10 different growth methods local function load(text) local result, err = loadstring(text) if( err ) then error(err, 3) return nil end return result() end local positionData = setmetatable({}, { __index = function(tbl, index) local data = {} local columnGrowth = ShadowUF.Layout:GetColumnGrowth(index) local auraGrowth = ShadowUF.Layout:GetAuraGrowth(index) data.xMod = (columnGrowth == "RIGHT" or auraGrowth == "RIGHT") and 1 or -1 data.yMod = (columnGrowth ~= "TOP" and auraGrowth ~= "TOP") and -1 or 1 local auraX, colX, auraY, colY, xOffset, yOffset, initialXOffset, initialYOffset = 0, 0, 0, 0, "", "", "", "" if( columnGrowth == "LEFT" or columnGrowth == "RIGHT" ) then colX = 1 xOffset = " + offset" initialXOffset = string.format(" + (%d * offset)", data.xMod) auraY = 3 data.isSideGrowth = true elseif( columnGrowth == "TOP" or columnGrowth == "BOTTOM" ) then colY = 2 yOffset = " + offset" initialYOffset = string.format(" + (%d * offset)", data.yMod) auraX = 2 end data.initialAnchor = load(string.format([[return function(button, offset) button:ClearAllPoints() button:SetPoint(button.point, button.anchorTo, button.relativePoint, button.xOffset%s, button.yOffset%s) end]], initialXOffset, initialYOffset)) data.column = load(string.format([[return function(button, positionTo, offset) button:ClearAllPoints() button:SetPoint("%s", positionTo, "%s", %d * (%d%s), %d * (%d%s)) end ]], ShadowUF.Layout:ReverseDirection(columnGrowth), columnGrowth, data.xMod, colX, xOffset, data.yMod, colY, yOffset)) data.aura = load(string.format([[return function(button, positionTo) button:ClearAllPoints() button:SetPoint("%s", positionTo, "%s", %d, %d) end ]], ShadowUF.Layout:ReverseDirection(auraGrowth), auraGrowth, data.xMod * auraX, data.yMod * auraY)) tbl[index] = data return tbl[index] end, }) local function positionButton(id, group, config) local position = positionData[group.forcedAnchorPoint or config.anchorPoint] local button = group.buttons[id] button.isAuraAnchor = nil -- Alright, in order to find out where an aura group is going to be anchored to certain buttons need -- to be flagged as suitable anchors visually, this speeds it up bcause this data is cached and doesn't -- have to be recalculated unless auras are specifically changed if( id > 1 ) then if( position.isSideGrowth and id <= config.perRow ) then button.isAuraAnchor = true end if( id % config.perRow == 1 or config.perRow == 1 ) then position.column(button, group.buttons[id - config.perRow], 0) if( not position.isSideGrowth ) then button.isAuraAnchor = true end else position.aura(button, group.buttons[id - 1]) end else button.isAuraAnchor = true button.point = ShadowUF.Layout:GetPoint(config.anchorPoint) button.relativePoint = ShadowUF.Layout:GetRelative(config.anchorPoint) button.xOffset = config.x + (position.xMod * ShadowUF.db.profile.backdrop.inset) button.yOffset = config.y + (position.yMod * ShadowUF.db.profile.backdrop.inset) button.anchorTo = group.anchorTo position.initialAnchor(button, 0) end end local columnsHaveScale = {} local function positionAllButtons(group, config) local position = positionData[group.forcedAnchorPoint or config.anchorPoint] -- Figure out which columns have scaling so we can work out positioning local columnID = 0 for id, button in pairs(group.buttons) do if( id % config.perRow == 1 or config.perRow == 1 ) then columnID = columnID + 1 columnsHaveScale[columnID] = nil end if( not columnsHaveScale[columnID] and button.isSelfScaled ) then local size = math.ceil(button:GetSize() * button:GetScale()) columnsHaveScale[columnID] = columnsHaveScale[columnID] and math.max(size, columnsHaveScale[columnID]) or size end end local columnID = 1 for id, button in pairs(group.buttons) do if( id > 1 ) then if( id % config.perRow == 1 or config.perRow == 1 ) then columnID = columnID + 1 local anchorButton = group.buttons[id - config.perRow] local previousScale, currentScale = columnsHaveScale[columnID - 1], columnsHaveScale[columnID] local offset = 0 -- Previous column has a scaled aura, and the button we are anchoring to is not scaled if( previousScale and not anchorButton.isSelfScaled ) then offset = (previousScale / 4) end -- Current column has a scaled aura, and the button isn't scaled if( currentScale and not button.isSelfScaled ) then offset = offset + (currentScale / 4) end -- Current anchor is scaled, previous is not if( button.isSelfScaled and not anchorButton.isSelfScaled ) then offset = offset - (currentScale / 6) end -- At least one of them is scaled if( ( not button.isSelfScaled or not anchorButton.isSelfScaled ) and offset > 0 ) then offset = offset + 1 end --print(columnID, math.ceil(offset)) position.column(button, anchorButton, math.ceil(offset)) else position.aura(button, group.buttons[id - 1]) end -- If the initial column is self scaled, but the initial anchor isn't, will have to reposition it elseif( columnsHaveScale[columnID] ) then local offset = math.ceil(columnsHaveScale[columnID] / 8) if( button.isSelfScaled ) then offset = -(offset / 2) else offset = offset + 2 end --print(1, offset) position.initialAnchor(button, offset) end end end -- Aura button functions -- Updates the X seconds left on aura tooltip while it's shown local function updateTooltip(self) if( GameTooltip:IsOwned(self) ) then GameTooltip:SetUnitAura(self.unit, self.auraID, self.filter) end end local function showTooltip(self) if( not ShadowUF.db.profile.locked ) then return end GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT") if( self.filter == "TEMP" ) then GameTooltip:SetInventoryItem("player", self.auraID) self:SetScript("OnUpdate", nil) else GameTooltip:SetUnitAura(self.unit, self.auraID, self.filter) self:SetScript("OnUpdate", updateTooltip) end end local function hideTooltip(self) self:SetScript("OnUpdate", nil) GameTooltip:Hide() end local function cancelBuff(self) if( not ShadowUF.db.profile.locked ) then return end if( self.filter == "TEMP" ) then CancelItemTempEnchantment(self.auraID - 15) else CancelUnitBuff(self.unit, self.auraID, self.filter) end end local function updateButton(id, group, config) local button = group.buttons[id] if( not button ) then group.buttons[id] = CreateFrame("Button", nil, group) button = group.buttons[id] button:SetScript("OnEnter", showTooltip) button:SetScript("OnLeave", hideTooltip) button:SetScript("OnClick", cancelBuff) button:RegisterForClicks("RightButtonUp") button.cooldown = CreateFrame("Cooldown", nil, button) button.cooldown:SetAllPoints(button) button.cooldown:SetReverse(true) button.cooldown:SetFrameLevel(7) button.cooldown:Hide() button.stack = button:CreateFontString(nil, "OVERLAY") button.stack:SetFont("Interface\\AddOns\\ShadowedUnitFrames\\media\\fonts\\Myriad Condensed Web.ttf", 10, "OUTLINE") button.stack:SetShadowColor(0, 0, 0, 1.0) button.stack:SetShadowOffset(0.50, -0.50) button.stack:SetHeight(1) button.stack:SetWidth(1) button.stack:SetAllPoints(button) button.stack:SetJustifyV("BOTTOM") button.stack:SetJustifyH("RIGHT") button.border = button:CreateTexture(nil, "OVERLAY") button.border:SetPoint("CENTER", button) button.icon = button:CreateTexture(nil, "BACKGROUND") button.icon:SetAllPoints(button) button.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) end if( ShadowUF.db.profile.auras.borderType == "" ) then button.border:Hide() elseif( ShadowUF.db.profile.auras.borderType == "blizzard" ) then button.border:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays") button.border:SetTexCoord(0.296875, 0.5703125, 0, 0.515625) button.border:Show() else button.border:SetTexture("Interface\\AddOns\\ShadowedUnitFrames\\media\\textures\\border-" .. ShadowUF.db.profile.auras.borderType) button.border:SetTexCoord(0, 1, 0, 1) button.border:Show() end -- Set the button sizing button.cooldown.noCooldownCount = ShadowUF.db.profile.omnicc button:SetHeight(config.size) button:SetWidth(config.size) button.border:SetHeight(config.size + 1) button.border:SetWidth(config.size + 1) button:ClearAllPoints() button:Hide() -- Position the button quickly positionButton(id, group, config) end -- Let the mover access this for creating aura things Auras.updateButton = updateButton -- Create an aura anchor as well as the buttons to contain it local function updateGroup(self, type, config, reverseConfig) self.auras[type] = self.auras[type] or CreateFrame("Frame", nil, self.highFrame) local group = self.auras[type] group.buttons = group.buttons or {} group.maxAuras = config.perRow * config.maxRows group.totalAuras = 0 group.temporaryEnchants = 0 group.type = type group.parent = self group.anchorTo = self group:SetFrameLevel(5) group:Show() -- If debuffs are anchored to buffs, debuffs need to grow however buffs do if( config.anchorOn and reverseConfig.enabled ) then group.forcedAnchorPoint = reverseConfig.anchorPoint end if( self.unit == "player" ) then mainHand.time = 0 offHand.time = 0 group:SetScript("OnUpdate", config.temporary and tempEnchantScan or nil) else group:SetScript("OnUpdate", nil) end -- Update filters used for the anchor group.filter = group.type == "buffs" and "HELPFUL" or group.type == "debuffs" and "HARMFUL" or "" -- This is a bit of an odd filter, when used with a HELPFUL filter, it will only return buffs you can cast on group members -- When used with HARMFUL it will only return debuffs you can cure if( config.raid ) then group.filter = group.filter .. "|RAID" end for id, button in pairs(group.buttons) do updateButton(id, group, config) end end -- Update aura positions based off of configuration function Auras:OnLayoutApplied(frame, config) if( frame.auras ) then if( frame.auras.buffs ) then for _, button in pairs(frame.auras.buffs.buttons) do button:Hide() end end if( frame.auras.debuffs ) then for _, button in pairs(frame.auras.debuffs.buttons) do button:Hide() end end end if( not frame.visibility.auras ) then return end if( config.auras.buffs.enabled ) then updateGroup(frame, "buffs", config.auras.buffs, config.auras.debuffs) end if( config.auras.debuffs.enabled ) then updateGroup(frame, "debuffs", config.auras.debuffs, config.auras.buffs) end -- Anchor an aura group to another aura group frame.auras.anchorAurasOn = nil if( config.auras.buffs.enabled and config.auras.debuffs.enabled ) then if( config.auras.buffs.anchorOn ) then frame.auras.anchorAurasOn = frame.auras.debuffs frame.auras.anchorAurasChild = frame.auras.buffs elseif( config.auras.debuffs.anchorOn ) then frame.auras.anchorAurasOn = frame.auras.buffs frame.auras.anchorAurasChild = frame.auras.debuffs end end -- Check if either auras are anchored to each other if( config.auras.buffs.anchorPoint == config.auras.debuffs.anchorPoint and config.auras.buffs.enabled and config.auras.debuffs.enabled and not config.auras.buffs.anchorOn and not config.auras.debuffs.anchorOn ) then frame.auras.anchor = frame.auras[config.auras.buffs.prioritize and "buffs" or "debuffs"] frame.auras.primary = config.auras.buffs.prioritize and "buffs" or "debuffs" frame.auras.secondary = frame.auras.primary == "buffs" and "debuffs" or "buffs" else frame.auras.anchor = nil end self:UpdateFilter(frame) end -- Temporary enchant support local timeElapsed = 0 local function updateTemporaryEnchant(frame, slot, tempData, hasEnchant, timeLeft, charges) -- If there's less than a 750 millisecond differences in the times, we don't need to bother updating. -- Any sort of enchant takes more than 0.750 seconds to cast so it's impossible for the user to have two -- temporary enchants with that little difference, as totems don't really give pulsing auras anymore. if( tempData.has and ( timeLeft < tempData.time and ( tempData.time - timeLeft ) < 750 ) and charges == tempData.charges ) then return false end -- Some trickys magic, we can't get the start time of temporary enchants easily. -- So will save the first time we find when a new enchant is added if( timeLeft > tempData.time or not tempData.has ) then tempData.startTime = GetTime() end tempData.has = hasEnchant tempData.time = timeLeft tempData.charges = charges local config = ShadowUF.db.profile.units[frame.parent.unitType].auras[frame.type] -- Create any buttons we need if( #(frame.buttons) < frame.temporaryEnchants ) then updateButton(frame.temporaryEnchants, frame, config) end local button = frame.buttons[frame.temporaryEnchants] -- Purple border button.border:SetVertexColor(0.50, 0, 0.50) -- Show the cooldown ring if( not ShadowUF.db.profile.auras.disableCooldown ) then button.cooldown:SetCooldown(tempData.startTime, timeLeft / 1000) button.cooldown:Show() end -- Enlarge our own auras if( config.enlargeSelf and caster == ShadowUF.playerUnit ) then button.isSelfScaled = true button:SetScale(config.selfScale) else button.isSelfScaled = nil button:SetScale(1) end -- Size it button:SetHeight(config.size) button:SetWidth(config.size) button.border:SetHeight(config.size + 1) button.border:SetWidth(config.size + 1) -- Stack + icon + show! Never understood why, auras sometimes return 1 for stack even if they don't stack button.auraID = slot button.filter = "TEMP" button.unit = nil button.columnHasScaled = nil button.previousHasScale = nil button.icon:SetTexture(GetInventoryItemTexture("player", slot)) button.stack:SetText(charges > 1 and charges or "") button:Show() end -- Unfortunately, temporary enchants have basically no support beyond hacks. So we will hack! tempEnchantScan = function(self, elapsed) timeElapsed = timeElapsed + elapsed if( timeElapsed < 0.50 ) then return end timeElapsed = timeElapsed - 0.50 local hasMain, mainTimeLeft, mainCharges, hasOff, offTimeLeft, offCharges = GetWeaponEnchantInfo() self.temporaryEnchants = 0 if( hasMain ) then self.temporaryEnchants = self.temporaryEnchants + 1 updateTemporaryEnchant(self, 16, mainHand, hasMain, mainTimeLeft or 0, mainCharges) mainHand.time = mainTimeLeft or 0 end mainHand.has = hasMain if( hasOff and self.temporaryEnchants < self.maxAuras ) then self.temporaryEnchants = self.temporaryEnchants + 1 updateTemporaryEnchant(self, 17, offHand, hasOff, offTimeLeft or 0, offCharges) offHand.time = offTimeLeft or 0 end offHand.has = hasOff -- Update if totals changed if( self.lastTemporary ~= self.temporaryEnchants ) then self.lastTemporary = self.temporaryEnchants Auras:Update(self.parent) end end -- Nice and simple, don't need to do a full update because either this is called in an OnEnable or -- the zone monitor will handle it all cleanly. The fun part of this code is aura filtering itself takes 10 seconds -- but making the configuration clean takes two weeks and another 2-3 days of implementing -- This isn't actually filled with data, it's just to stop any errors from triggering if no filter is added local filterDefault = {} function Auras:UpdateFilter(frame) local zone = select(2, IsInInstance()) local id = zone .. frame.unitType local white = ShadowUF.db.profile.filters.zonewhite[zone .. frame.unitType] local black = ShadowUF.db.profile.filters.zoneblack[zone .. frame.unitType] frame.auras.whitelist = white and ShadowUF.db.profile.filters.whitelists[white] or filterDefault frame.auras.blacklist = black and ShadowUF.db.profile.filters.blacklists[black] or filterDefault end -- Scan for auras local function scan(parent, frame, type, config, filter) if( frame.totalAuras >= frame.maxAuras or not config.enabled ) then return end -- CoA: |RAID is not honoured for custom classes; override with manual type check. local coaFilter if filter == "HARMFUL|RAID" then coaFilter = getCoaDispels() if coaFilter then filter = "HARMFUL" end end local isFriendly = UnitIsFriend(frame.parent.unit, "player") local index = 0 while( true ) do index = index + 1 -- CoA's 3.3.5 client returns spellId as the 11th value (stock 3.3.5a stops at 10), -- which lets the whitelist/blacklist match by ID as well as by name. local name, rank, texture, count, auraType, duration, endTime, caster, isStealable, _, spellId = UnitAura(frame.parent.unit, index, filter) if( not name ) then break end if( ( not coaFilter or (auraType and coaFilter[auraType]) ) and ( not config.player or playerUnits[caster] ) and ( not parent.whitelist[type] and not parent.blacklist[type] or parent.whitelist[type] and ( parent.whitelist[name] or parent.whitelist[spellId] ) or parent.blacklist[type] and not ( parent.blacklist[name] or parent.blacklist[spellId] ) ) ) then -- Create any buttons we need frame.totalAuras = frame.totalAuras + 1 if( #(frame.buttons) < frame.totalAuras ) then updateButton(frame.totalAuras, frame, ShadowUF.db.profile.units[frame.parent.unitType].auras[frame.type]) end -- Show debuff border, or a special colored border if it's stealable local button = frame.buttons[frame.totalAuras] if( isStealable and not isFriendly and not ShadowUF.db.profile.auras.disableColor ) then button.border:SetVertexColor(stealableColor.r, stealableColor.g, stealableColor.b) elseif( ( not isFriendly or type == "debuffs" ) and not ShadowUF.db.profile.auras.disableColor ) then local color = auraType and DebuffTypeColor[auraType] or DebuffTypeColor.none button.border:SetVertexColor(color.r, color.g, color.b) else button.border:SetVertexColor(0.60, 0.60, 0.60) end -- Show the cooldown ring if( not ShadowUF.db.profile.auras.disableCooldown and duration > 0 and endTime > 0 and ( not config.selfTimers or ( config.selfTimers and playerUnits[caster] ) ) ) then button.cooldown:SetCooldown(endTime - duration, duration) button.cooldown:Show() else button.cooldown:Hide() end -- Enlarge our own auras if( config.enlargeSelf and playerUnits[caster] ) then button.isSelfScaled = true button:SetScale(config.selfScale) else button.isSelfScaled = nil button:SetScale(1) end -- Size it button:SetHeight(config.size) button:SetWidth(config.size) button.border:SetHeight(config.size + 1) button.border:SetWidth(config.size + 1) -- Stack + icon + show! Never understood why, auras sometimes return 1 for stack even if they don't stack button.auraID = index button.filter = filter button.unit = frame.parent.unit button.columnHasScaled = nil button.previousHasScale = nil button.icon:SetTexture(texture) button.stack:SetText(count > 1 and count or "") button:Show() -- Too many auras shown break out -- Get down if( frame.totalAuras >= frame.maxAuras ) then break end end end for i=frame.totalAuras + 1, #(frame.buttons) do frame.buttons[i]:Hide() end -- The default 1.30 scale doesn't need special handling, after that it does if( config.enlargeSelf ) then positionAllButtons(frame, config) end end Auras.scan = scan local function anchorGroupToGroup(frame, config, group, childConfig, childGroup) -- Child group has nothing in it yet, so don't care if( not childGroup.buttons[1] ) then return end -- Group we want to anchor to has nothing in it, takeover the postion if( group.totalAuras == 0 ) then local position = positionData[config.anchorPoint] childGroup.buttons[1]:ClearAllPoints() childGroup.buttons[1]:SetPoint(ShadowUF.Layout:GetPoint(config.anchorPoint), group.anchorTo, ShadowUF.Layout:GetRelative(config.anchorPoint), config.x + (position.xMod * ShadowUF.db.profile.backdrop.inset), config.y + (position.yMod * -ShadowUF.db.profile.backdrop.inset)) return end local anchorTo for i=#(group.buttons), 1, -1 do local button = group.buttons[i] if( button.isAuraAnchor and button:IsVisible() ) then anchorTo = button break end end local position = positionData[childGroup.forcedAnchorPoint or childConfig.anchorPoint] if( position.isSideGrowth ) then position.aura(childGroup.buttons[1], anchorTo) else position.column(childGroup.buttons[1], anchorTo, 2) end end Auras.anchorGroupToGroup = anchorGroupToGroup -- Do an update and figure out what we need to scan function Auras:Update(frame) local config = ShadowUF.db.profile.units[frame.unitType].auras if( frame.auras.anchor ) then frame.auras.anchor.totalAuras = frame.auras.anchor.temporaryEnchants scan(frame.auras, frame.auras.anchor, frame.auras.primary, config[frame.auras.primary], frame.auras[frame.auras.primary].filter) scan(frame.auras, frame.auras.anchor, frame.auras.secondary, config[frame.auras.secondary], frame.auras[frame.auras.secondary].filter) else if( config.buffs.enabled ) then frame.auras.buffs.totalAuras = frame.auras.buffs.temporaryEnchants scan(frame.auras, frame.auras.buffs, "buffs", config.buffs, frame.auras.buffs.filter) end if( config.debuffs.enabled ) then frame.auras.debuffs.totalAuras = 0 scan(frame.auras, frame.auras.debuffs, "debuffs", config.debuffs, frame.auras.debuffs.filter) end if( frame.auras.anchorAurasOn ) then anchorGroupToGroup(frame, config[frame.auras.anchorAurasOn.type], frame.auras.anchorAurasOn, config[frame.auras.anchorAurasChild.type], frame.auras.anchorAurasChild) end end end