NanamiPlates_Auras = {} local NP = NanamiPlates local Settings = NP.Settings local SpellDB = NanamiPlates_SpellDB local pairs = pairs local ipairs = ipairs local string_find = string.find local string_format = string.format local string_gsub = string.gsub local math_floor = math.floor local GetTime = GetTime local UnitDebuff = UnitDebuff local UnitGUID = UnitGUID local UnitLevel = UnitLevel local UnitName = UnitName local UnitExists = UnitExists local CreateFrame = CreateFrame local _, playerClass = UnitClass("player") playerClass = playerClass or "" local MAX_DEBUFFS = 16 local JUDGEMENT_EFFECTS = { "Judgement of Wisdom", "Judgement of Light", "Judgement of the Crusader", "Judgement of Justice", "Judgement" } NanamiPlates_Auras.timers = {} local debuffTimers = NanamiPlates_Auras.timers local function FormatTime(remaining) if not remaining or remaining < 0 then return "", 1, 1, 1, 1 end if remaining > 3600 then return math_floor(remaining / 3600 + 0.5) .. "h", 0.5, 0.5, 0.5, 1 elseif remaining > 60 then return math_floor(remaining / 60 + 0.5) .. "m", 0.5, 0.5, 0.5, 1 elseif remaining > 10 then return math_floor(remaining + 0.5) .. "", 0.7, 0.7, 0.7, 1 elseif remaining > 5 then return math_floor(remaining + 0.5) .. "", 1, 1, 0, 1 elseif remaining > 0 then return string_format("%.1f", remaining), 1, 0, 0, 1 end return "", 1, 1, 1, 1 end NanamiPlates_Auras.FormatTime = FormatTime local function GetSpellData(unit, name, effect, level) if not SpellDB or not SpellDB.objects then return nil end local dataUnit = unit and SpellDB:FindEffectData(unit, level or 0, effect) local dataName = name and SpellDB:FindEffectData(name, level or 0, effect) if dataUnit and dataName then return (dataUnit.start or 0) >= (dataName.start or 0) and dataUnit or dataName end return dataUnit or dataName end local function DebuffOnUpdate() local now = GetTime() if (this.tick or 0) > now then return else this.tick = now + 0.1 end if not this:IsShown() then return end if not this.expirationTime or this.expirationTime <= 0 then if this.cd then this.cd:SetText("") end return end local timeLeft = this.expirationTime - now if timeLeft > 0 then local text, r, g, b, a = FormatTime(timeLeft) if this.cd then this.cd:SetText(text) if r then this.cd:SetTextColor(r, g, b, a or 1) end this.cd:SetAlpha(1) end else if this.cd then this.cd:SetText("") this.cd:SetAlpha(0) end this.expirationTime = 0 end end function NanamiPlates_Auras:CreateDebuffFrames(nameplate) nameplate.debuffs = {} local plateName = nameplate:GetName() or "UnknownPlate" local size = Settings.debuffIconSize or 20 for i = 1, MAX_DEBUFFS do local debuff = CreateFrame("Frame", plateName .. "Debuff" .. i, nameplate) debuff:SetWidth(size) debuff:SetHeight(size) debuff:SetFrameLevel(nameplate.health:GetFrameLevel() + 5) debuff:EnableMouse(false) debuff.icon = debuff:CreateTexture(nil, "ARTWORK") debuff.icon:SetAllPoints() debuff.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) debuff.icon:SetDrawLayer("ARTWORK") debuff.border = debuff:CreateTexture(nil, "BACKGROUND") debuff.border:SetTexture(0, 0, 0, 1) debuff.border:SetPoint("TOPLEFT", debuff, "TOPLEFT", -1, 1) debuff.border:SetPoint("BOTTOMRIGHT", debuff, "BOTTOMRIGHT", 1, -1) debuff.border:SetDrawLayer("BACKGROUND") debuff.cdframe = CreateFrame("Frame", nil, debuff) debuff.cdframe:SetAllPoints(debuff) debuff.cdframe:SetFrameLevel(debuff:GetFrameLevel() + 2) debuff.cdframe:EnableMouse(false) debuff.cd = debuff.cdframe:CreateFontString(nil, "OVERLAY") debuff.cd:SetFont(NP.GetFont(), 10, NP.GetFontOutline()) debuff.cd:SetPoint("BOTTOM", debuff, "BOTTOM", 0, -1) debuff.cd:SetTextColor(1, 1, 0, 1) debuff.cd:SetText("") debuff.cd:SetDrawLayer("OVERLAY", 7) debuff.count = debuff.cdframe:CreateFontString(nil, "OVERLAY") debuff.count:SetFont(NP.GetFont(), 9, NP.GetFontOutline()) debuff.count:SetPoint("TOPRIGHT", debuff, "TOPRIGHT", 2, 2) debuff.count:SetTextColor(1, 1, 1, 1) debuff.count:SetText("") debuff.count:SetDrawLayer("OVERLAY", 7) debuff:SetScript("OnUpdate", DebuffOnUpdate) debuff:Hide() nameplate.debuffs[i] = debuff end end function NanamiPlates_Auras:UpdateDebuffs(nameplate, unitstr, plateName, isTarget, hasValidGUID, superwow_active) local size = Settings.debuffIconSize or 20 for i = 1, MAX_DEBUFFS do local debuff = nameplate.debuffs[i] debuff:SetWidth(size) debuff:SetHeight(size) local cdFontSize = math_floor(size * 0.5 + 0.5) local countFontSize = math_floor(size * 0.4 + 0.5) if cdFontSize < 7 then cdFontSize = 7 end if countFontSize < 6 then countFontSize = 6 end debuff.cd:SetFont(NP.GetFont(), cdFontSize, NP.GetFontOutline()) debuff.count:SetFont(NP.GetFont(), countFontSize, NP.GetFontOutline()) debuff:Hide() debuff.count:SetText("") debuff.expirationTime = 0 end local now = GetTime() local claimedMyDebuffs = {} local effectiveUnit = (isTarget) and "target" or (superwow_active and hasValidGUID and unitstr) or nil if not effectiveUnit and not plateName then return 0 end local scanUnit = effectiveUnit if not scanUnit and plateName and UnitExists("target") and UnitName("target") == plateName then scanUnit = "target" end if not scanUnit then return 0 end -- Collect debuffs local collectedDebuffs = {} local ownerBoundCounts = {} local ownerBoundFirst = {} for i = 1, 40 do local texture, stacks = UnitDebuff(scanUnit, i) if not texture then break end local effect = SpellDB and SpellDB:ScanDebuff(scanUnit, i) if (not effect or effect == "") and SpellDB and SpellDB.textureToSpell then effect = SpellDB.textureToSpell[texture] end -- If effect is non-empty but not in DEBUFFS (e.g. Chinese name), try fallbacks if SpellDB and effect and effect ~= "" and SpellDB.DEBUFFS and not SpellDB.DEBUFFS[effect] then -- Try texture-based lookup (covers spellbook-scanned entries) local texEffect = SpellDB.textureToSpell and SpellDB.textureToSpell[texture] if texEffect and SpellDB.DEBUFFS[texEffect] then -- Learn this locale mapping for future if SpellDB.LearnLocale then SpellDB:LearnLocale(effect, texEffect) end effect = texEffect elseif SpellDB.WARLOCK_DOT_TEXTURES and SpellDB.WARLOCK_DOT_TEXTURES[texture] then effect = SpellDB.WARLOCK_DOT_TEXTURES[texture] elseif SpellDB.WARLOCK_CURSE_TEXTURES and SpellDB.WARLOCK_CURSE_TEXTURES[texture] then effect = SpellDB.WARLOCK_CURSE_TEXTURES[texture] end end local isOwnerBound = effect and SpellDB and SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[effect] if isOwnerBound then ownerBoundCounts[effect] = (ownerBoundCounts[effect] or 0) + 1 if not ownerBoundFirst[effect] then ownerBoundFirst[effect] = { index = i, texture = texture, stacks = stacks } end end table.insert(collectedDebuffs, { index = i, texture = texture, stacks = stacks, effect = effect, isOwnerBound = isOwnerBound }) end -- Display debuffs local debuffIndex = 1 local displayedOwnerBound = {} local unitlevel = (scanUnit == "target") and UnitLevel("target") or (unitstr and UnitLevel(unitstr)) or 0 for _, debuffData in ipairs(collectedDebuffs) do if debuffIndex > MAX_DEBUFFS then break end local effect = debuffData.effect local texture = debuffData.texture local stacks = debuffData.stacks local isOwnerBound = debuffData.isOwnerBound local isRoguePoison = false if playerClass == "ROGUE" then if effect and SpellDB.ROGUE_POISONS and SpellDB.ROGUE_POISONS[effect] then isRoguePoison = true elseif texture and SpellDB.ROGUE_POISON_TEXTURES and SpellDB.ROGUE_POISON_TEXTURES[texture] then isRoguePoison = true if not effect or effect == "" then effect = SpellDB.ROGUE_POISON_TEXTURES[texture] end end end local isHunterTrap = false if playerClass == "HUNTER" then if effect and SpellDB.HUNTER_TRAPS and SpellDB.HUNTER_TRAPS[effect] then isHunterTrap = true elseif texture and SpellDB.HUNTER_TRAP_TEXTURES and SpellDB.HUNTER_TRAP_TEXTURES[texture] then isHunterTrap = true effect = SpellDB.HUNTER_TRAP_TEXTURES[texture] end end local isHunterSting = false if playerClass == "HUNTER" then if effect and SpellDB.HUNTER_STINGS and SpellDB.HUNTER_STINGS[effect] then isHunterSting = true elseif texture and SpellDB.HUNTER_STING_TEXTURES and SpellDB.HUNTER_STING_TEXTURES[texture] then isHunterSting = true effect = SpellDB.HUNTER_STING_TEXTURES[texture] end end local isWarlockCurse = false if playerClass == "WARLOCK" then if effect and SpellDB.WARLOCK_CURSES and SpellDB.WARLOCK_CURSES[effect] then isWarlockCurse = true elseif texture and SpellDB.WARLOCK_CURSE_TEXTURES and SpellDB.WARLOCK_CURSE_TEXTURES[texture] then isWarlockCurse = true effect = SpellDB.WARLOCK_CURSE_TEXTURES[texture] end end local isWarlockDot = false if playerClass == "WARLOCK" and not isWarlockCurse then if texture and SpellDB.WARLOCK_DOT_TEXTURES and SpellDB.WARLOCK_DOT_TEXTURES[texture] then local dotName = SpellDB.WARLOCK_DOT_TEXTURES[texture] if not effect or effect == "" or not SpellDB.DEBUFFS[effect] then effect = dotName end isWarlockDot = true elseif effect and (effect == "Corruption" or effect == "Siphon Life" or effect == "Immolate" or effect == "Dark Harvest") then isWarlockDot = true end end if isWarlockDot or isWarlockCurse then isOwnerBound = effect and SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[effect] end local isMyDebuff = false local duration, timeleft = nil, nil if isRoguePoison or isHunterTrap or isHunterSting or isWarlockCurse or isWarlockDot then isMyDebuff = true end if effect and effect ~= "" then local data = GetSpellData(unitstr, plateName, effect, unitlevel) if data and data.start and data.duration then if data.start + data.duration > now then duration = data.duration timeleft = data.duration + data.start - now if data.isOwn == true and not claimedMyDebuffs[effect] then isMyDebuff = true claimedMyDebuffs[effect] = true end end end if playerClass == "PALADIN" and (string_find(effect, "Judgement of ") or string_find(effect, "Seal of ") or effect == "Crusader Strike" or effect == "Hammer of Justice" or effect == "Repentance") then isMyDebuff = true claimedMyDebuffs[effect] = true end if not timeleft then local dbDuration = SpellDB:GetDuration(effect, 0) if dbDuration > 0 then SpellDB:AddEffect(plateName, unitlevel, effect, dbDuration, isMyDebuff) if unitstr and unitstr ~= plateName then SpellDB:AddEffect(unitstr, unitlevel, effect, dbDuration, isMyDebuff) end end end end if effect and effect ~= "" and not duration then duration = SpellDB:GetDuration(effect, 0) end -- Display logic if isOwnerBound then if not displayedOwnerBound[effect] then local ownerCheckUnit = unitstr or plateName local isMyOwnerBound = isMyDebuff if not isMyOwnerBound and SpellDB.IsOwnerBoundDebuffMine then isMyOwnerBound = SpellDB:IsOwnerBoundDebuffMine(ownerCheckUnit, effect) if not isMyOwnerBound and plateName and plateName ~= ownerCheckUnit then isMyOwnerBound = SpellDB:IsOwnerBoundDebuffMine(plateName, effect) end end local shouldShowOwnerBound = isMyOwnerBound or not Settings.showOnlyMyDebuffs if shouldShowOwnerBound then displayedOwnerBound[effect] = true local debuff = nameplate.debuffs[debuffIndex] debuff.icon:SetTexture(texture) local instanceCount = ownerBoundCounts[effect] or 1 if instanceCount > 1 then debuff.count:SetText(instanceCount) debuff.count:SetTextColor(0.3, 0.7, 1, 1) elseif stacks and stacks > 1 then debuff.count:SetText(stacks) else debuff.count:SetText("") end local debuffKey = (unitstr or plateName) .. "_" .. effect local displayTimeLeft = nil if timeleft and timeleft > 0 then displayTimeLeft = timeleft debuffTimers[debuffKey] = { startTime = now - (duration - timeleft), duration = duration, lastSeen = now } else local fallbackDuration = duration or SpellDB:GetDuration(effect, 0) if fallbackDuration <= 0 then fallbackDuration = 30 end if not debuffTimers[debuffKey] then debuffTimers[debuffKey] = { startTime = now, duration = fallbackDuration, lastSeen = now } end local cached = debuffTimers[debuffKey] cached.lastSeen = now if (now - cached.startTime) > cached.duration then cached.startTime = now cached.duration = fallbackDuration end displayTimeLeft = cached.duration - (now - cached.startTime) end if Settings.showDebuffTimers and displayTimeLeft and displayTimeLeft > 0 then debuff.expirationTime = now + displayTimeLeft local text, r, g, b, a = FormatTime(displayTimeLeft) debuff.cd:SetText(text) if r then debuff.cd:SetTextColor(r, g, b, a) end debuff.cdframe:Show() else debuff.expirationTime = 0 debuff.cd:SetText("") end debuff:Show() debuffIndex = debuffIndex + 1 end end else local uniqueClass = effect and SpellDB and SpellDB.SHARED_DEBUFFS and SpellDB.SHARED_DEBUFFS[effect] local isUnique = uniqueClass and (uniqueClass == true or uniqueClass == playerClass) local shouldDisplay = true if Settings.showOnlyMyDebuffs and not isMyDebuff and not isUnique and not isOwnerBound and not isHunterTrap then shouldDisplay = false end if shouldDisplay then local debuff = nameplate.debuffs[debuffIndex] debuff.icon:SetTexture(texture) debuff.count:SetText((stacks and stacks > 1) and stacks or "") local debuffKey = (unitstr or plateName) .. "_" .. (effect or texture) local displayTimeLeft = nil if timeleft and timeleft > 0 then displayTimeLeft = timeleft debuffTimers[debuffKey] = { startTime = now - (duration - timeleft), duration = duration, lastSeen = now } else local dbDur = (effect and effect ~= "") and SpellDB:GetDuration(effect, 0) or 0 local fallbackDuration = (duration and duration > 0 and duration) or (dbDur > 0 and dbDur) or 12 if not debuffTimers[debuffKey] then debuffTimers[debuffKey] = { startTime = now, duration = fallbackDuration, lastStacks = stacks or 0 } end local cached = debuffTimers[debuffKey] cached.lastSeen = now local stacksChanged = stacks and cached.lastStacks and stacks ~= cached.lastStacks if fallbackDuration > 1 and (cached.duration ~= fallbackDuration or (now - cached.startTime) > cached.duration or stacksChanged) then cached.duration = fallbackDuration cached.startTime = now end cached.lastStacks = stacks or 0 displayTimeLeft = cached.duration - (now - cached.startTime) end if Settings.showDebuffTimers and displayTimeLeft and displayTimeLeft > 0 then debuff.expirationTime = now + displayTimeLeft local text, r, g, b, a = FormatTime(displayTimeLeft) debuff.cd:SetText(text) if r then debuff.cd:SetTextColor(r, g, b, a) end debuff.cdframe:Show() else debuff.expirationTime = 0 debuff.cd:SetText("") end debuff:Show() debuffIndex = debuffIndex + 1 end end end return debuffIndex - 1 end function NanamiPlates_Auras:UpdateDebuffPositions(nameplate, numDebuffs) if numDebuffs <= 0 then return end local anchor = nameplate.debuffAnchor or nameplate.healthBG for i = 1, numDebuffs do local debuff = nameplate.debuffs[i] if debuff then debuff:ClearAllPoints() if i == 1 then debuff:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2) else debuff:SetPoint("LEFT", nameplate.debuffs[i - 1], "RIGHT", 1, 0) end end end end local lastDebuffCleanup = 0 local lastOwnerBoundCleanup = 0 function NanamiPlates_Auras:CleanupTimers() local now = GetTime() if now - lastDebuffCleanup > 3 then lastDebuffCleanup = now for key, data in pairs(self.timers) do local expired = false if data.lastSeen and (now - data.lastSeen > 5) then expired = true end if data.startTime and data.duration and (now - data.startTime > data.duration + 10) then expired = true end if expired then self.timers[key] = nil end end end if now - lastOwnerBoundCleanup > 10 then lastOwnerBoundCleanup = now if SpellDB and SpellDB.CleanupOwnerBoundCache then SpellDB:CleanupOwnerBoundCache() end end end -- Paladin judgement refresh function NanamiPlates_Auras:RefreshJudgementsOnTarget() if playerClass ~= "PALADIN" then return end if not UnitExists("target") then return end if not SpellDB or not SpellDB.objects then return end local name = UnitName("target") local level = UnitLevel("target") or 0 local guid = UnitGUID and UnitGUID("target") local hasNameData = name and SpellDB.objects[name] local hasGuidData = guid and SpellDB.objects[guid] if not hasNameData and not hasGuidData then return end for _, effect in ipairs(JUDGEMENT_EFFECTS) do local found = false local dur = SpellDB:GetDuration(effect, 0) or 10 if hasNameData then for lvl, effects in pairs(SpellDB.objects[name]) do if effects[effect] then effects[effect].start = GetTime() effects[effect].duration = dur found = true break end end end if hasGuidData and not found then for lvl, effects in pairs(SpellDB.objects[guid]) do if effects[effect] then effects[effect].start = GetTime() effects[effect].duration = dur found = true break end end end if found then debuffTimers[name .. "_" .. effect] = nil if guid then debuffTimers[guid .. "_" .. effect] = nil end end end end function NanamiPlates_Auras:SealHandler(attacker, victim) if playerClass ~= "PALADIN" then return end local isOwn = (attacker == "You" or attacker == UnitName("player")) if not isOwn then return end self:RefreshJudgementsOnTarget() end function NanamiPlates_Auras:HolyStrikeHandler(msg) if not msg or playerClass ~= "PALADIN" then return end local holyStrike = string_find(string.sub(msg, 6, 17), "Holy Strike") if not holyStrike then return end if not string_find(msg, "%d+") then return end self:RefreshJudgementsOnTarget() end NanamiPlates.Auras = NanamiPlates_Auras