diff --git a/api/unitframes.lua b/api/unitframes.lua new file mode 100644 index 0000000..5312079 --- /dev/null +++ b/api/unitframes.lua @@ -0,0 +1,3418 @@ +-- load pfUI environment +setfenv(1, pfUI:GetEnvironment()) + +pfUI.uf = CreateFrame("Frame", nil, UIParent) +pfUI.uf:SetScript("OnUpdate", function() + if InCombatLockdown and not InCombatLockdown() then + for frame in pairs(pfUI.uf.delayed) do + frame:UpdateVisibility() + pfUI.uf.delayed[frame] = nil + end + end +end) + +pfUI.uf.frames = {} +pfUI.uf.delayed = {} + +-- ============================================================================ +-- GUID-based Roster Tracking for Smart Updates +-- Only updates frames where the unit actually changed, not ALL 40 frames +-- ============================================================================ +pfUI.uf.guidTracker = { + -- Maps frame to its last known GUID: frame -> guid + frameToGuid = {}, +} + +-- Clear all GUID tracking (forces full update next time) +function pfUI.uf.ClearGuidTracking() + pfUI.uf.guidTracker.frameToGuid = {} +end + +-- slash command to toggle unitframe test mode +_G.SLASH_PFTEST1, _G.SLASH_PFTEST2 = "/pftest", "/pfuftest" +_G.SlashCmdList.PFTEST = function() + pfUI.uf.showall = not pfUI.uf.showall +end + +local scanner +local glow = { + edgeFile = pfUI.media["img:glow"], edgeSize = 8, + insets = {left = 0, right = 0, top = 0, bottom = 0}, +} + +local glow2 = { + edgeFile = pfUI.media["img:glow2"], edgeSize = 8, + insets = {left = 0, right = 0, top = 0, bottom = 0}, +} + +local function DoNothing() + return +end + +local maxdurations = {} +local function BuffOnUpdate() + if ( this.tick or 1) > GetTime() then return else this.tick = GetTime() + .2 end + local bid = GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HELPFUL") + local timeleft = GetPlayerBuffTimeLeft(bid) + local texture = GetPlayerBuffTexture(bid) + local start = 0 + + -- slot is empty (buff expired or doesn't exist), clear timer and bail + if not texture then + CooldownFrame_SetTimer(this.cd, 0, 0, 0) + return + end + + -- Get buff name for unique key (two buffs could share same texture) + local name = "" + if libtipscan then + scanner = scanner or libtipscan:GetScanner("unitframes") + if scanner then + scanner:SetPlayerBuff(bid) + name = scanner:Line(1) or "" + end + end + local key = texture .. name + + if timeleft > 0 then + if not maxdurations[key] then + maxdurations[key] = timeleft + elseif maxdurations[key] and maxdurations[key] < timeleft then + maxdurations[key] = timeleft + end + start = GetTime() + timeleft - maxdurations[key] + end + + CooldownFrame_SetTimer(this.cd, start, maxdurations[key], timeleft > 0 and 1 or 0) +end + +local function TargetBuffOnUpdate() + local name, rank, icon, count, duration, timeleft = _G.UnitBuff("target", this.id) + if duration and timeleft then + CooldownFrame_SetTimer(this.cd, GetTime() + timeleft - duration, duration, 1) + else + CooldownFrame_SetTimer(this.cd, 0, 0, 0) + end +end + +local function BuffOnEnter() + local parent = this:GetParent() + if not parent.label then return end + + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + if parent.label == "player" then + GameTooltip:SetPlayerBuff(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HELPFUL")) + else + GameTooltip:SetUnitBuff(parent.label .. parent.id, this.id) + end + + if IsShiftKeyDown() then + local texture = parent.label == "player" and GetPlayerBuffTexture(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HELPFUL")) or UnitBuff(parent.label .. parent.id, this.id) + + -- slot is empty, nothing to compare against + if not texture then return end + + local playerlist = "" + local first = true + + if UnitInRaid("player") then + for i=1,40 do + local unitstr = "raid" .. i + if not UnitHasBuff(unitstr, texture) and UnitName(unitstr) then + playerlist = playerlist .. ( not first and ", " or "") .. GetUnitColor(unitstr) .. UnitName(unitstr) .. "|r" + first = nil + end + end + else + if not UnitHasBuff("player", texture) then + playerlist = playerlist .. ( not first and ", " or "") .. GetUnitColor("player") .. UnitName("player") .. "|r" + first = nil + end + + for i=1,4 do + local unitstr = "party" .. i + if not UnitHasBuff(unitstr, texture) and UnitName(unitstr) then + playerlist = playerlist .. ( not first and ", " or "") .. GetUnitColor(unitstr) .. UnitName(unitstr) .. "|r" + first = nil + end + end + end + + if strlen(playerlist) > 0 then + GameTooltip:AddLine(" ") + GameTooltip:AddLine(T["Unbuffed"] .. ":", .3, 1, .8) + GameTooltip:AddLine(playerlist,1,1,1,1) + GameTooltip:Show() + end + end +end + +local function BuffOnLeave() + GameTooltip:Hide() +end + +local function BuffOnClick() + if this:GetParent().label == "player" then + CancelPlayerBuff(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HELPFUL")) + end +end + +local function DebuffOnUpdate() + if ( this.tick or 1) > GetTime() then return else this.tick = GetTime() + .2 end + local bid = GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HARMFUL") + local timeleft = GetPlayerBuffTimeLeft(bid) + local texture = GetPlayerBuffTexture(bid) + local start = 0 + + -- slot is empty (debuff expired or doesn't exist), clear timer and bail + if not texture then + CooldownFrame_SetTimer(this.cd, 0, 0, 0) + return + end + + -- Get debuff name for unique key (two debuffs could share same texture) + local name = "" + if libtipscan then + scanner = scanner or libtipscan:GetScanner("unitframes") + if scanner then + scanner:SetPlayerBuff(bid) + name = scanner:Line(1) or "" + end + end + local key = texture .. name + + if timeleft > 0 then + if not maxdurations[key] then + maxdurations[key] = timeleft + elseif maxdurations[key] and maxdurations[key] < timeleft then + maxdurations[key] = timeleft + end + start = GetTime() + timeleft - maxdurations[key] + end + + CooldownFrame_SetTimer(this.cd, start, maxdurations[key], timeleft > 0 and 1 or 0) +end + +local function DebuffOnEnter() + if not this:GetParent().label then return end + + GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") + if this:GetParent().label == "player" then + GameTooltip:SetPlayerBuff(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HARMFUL")) + else + local unitstr = this:GetParent().label .. this:GetParent().id + local parent = this:GetParent() + + -- For "only own debuffs" mode: find the REAL slot by matching spell name AND caster + if parent.config and parent.config.selfdebuff == "1" and libdebuff then + -- Get the spell name from our filtered list + local ownDebuffName = libdebuff:UnitOwnDebuff(unitstr, this.id) + + if ownDebuffName then + -- Search through all game slots to find OUR debuff with matching name + for gameSlot = 1, 16 do + local gameName, _, _, _, _, _, _, gameCaster = libdebuff:UnitDebuff(unitstr, gameSlot) + -- Match both name AND caster (must be ours) + if gameName == ownDebuffName and gameCaster == "player" then + GameTooltip:SetUnitDebuff(unitstr, gameSlot) + return + end + end + end + end + + -- Normal mode: use visual id directly + GameTooltip:SetUnitDebuff(unitstr, this.id) + end +end + +local function DebuffOnLeave() + GameTooltip:Hide() +end + +local function DebuffOnClick() + if this:GetParent().label == "player" then + CancelPlayerBuff(GetPlayerBuff(PLAYER_BUFF_START_ID+this.id,"HARMFUL")) + end +end + +local visibilityscan = CreateFrame("Frame", "pfUnitFrameVisibility", UIParent) +visibilityscan.frames = {} +visibilityscan:SetScript("OnUpdate", function() + if ( this.limit or 1) > GetTime() then return else this.limit = GetTime() + .2 end + for frame in pairs(this.frames) do frame:UpdateVisibility() end +end) + +-- ============================================================================ +-- GetUnitStats - Nampower Integration for Health + Power +-- Returns: hp, maxHp, power, maxPower, powerType +-- IMPORTANT: Uses _G.UnitExists directly to avoid conflicts with Nampower's +-- use GetUnitGUID(unit) for GUID lookup (Nampower 3.0.0+) +-- ============================================================================ + +-- Cache für Stats-Tracking (nur Änderungen zählen) +pfUI.api.lastUnitStats = pfUI.api.lastUnitStats or {} + +function pfUI.api.GetUnitStats(unitstr, trackStats) + local hp, maxHp, power, maxPower, powerType + local usedNampower = false + + + -- Try GetUnitField first if available (for all units: players, pets, NPCs) + if GetUnitField then + -- Use the standard check first, then get guid separately + local exists = _G.UnitExists(unitstr) + if exists then + -- Get guid via the extended UnitExists for Nampower + local _, guid = _G.UnitExists(unitstr) + + if guid then + hp = GetUnitField(guid, "health") + maxHp = GetUnitField(guid, "maxHealth") + + -- Get power type from bytes0 + local bytes0 = GetUnitField(guid, "bytes0") + if bytes0 then + local temp = math.floor(bytes0 / 16777216) + powerType = temp - math.floor(temp / 256) * 256 + else + powerType = UnitPowerType(unitstr) or 0 + end + + -- Get power values based on type + if powerType == 1 then + -- Rage (Nampower stores rage * 10, maxPower2 stores maxRage * 10) + local rage = GetUnitField(guid, "power2") + local maxRage = GetUnitField(guid, "maxPower2") + power = rage and math.floor(rage / 10) or UnitMana(unitstr) + maxPower = maxRage and math.floor(maxRage / 10) or UnitManaMax(unitstr) + elseif powerType == 3 then + -- Energy + power = GetUnitField(guid, "power4") or UnitMana(unitstr) + if power then power = math.floor(power) end + maxPower = GetUnitField(guid, "maxPower4") or UnitManaMax(unitstr) + elseif powerType == 2 then + -- Focus (Hunter pets use power3) + power = GetUnitField(guid, "power3") or UnitMana(unitstr) + if power then power = math.floor(power) end + maxPower = GetUnitField(guid, "maxPower3") or UnitManaMax(unitstr) + + else + -- Mana (default) + power = GetUnitField(guid, "power1") or UnitMana(unitstr) + maxPower = GetUnitField(guid, "maxPower1") or UnitManaMax(unitstr) + end + + -- Check if Nampower gave valid health data + if hp and hp > 0 and maxHp and maxHp > 0 then + usedNampower = true + + -- Track Nampower success - NUR bei echten Änderungen + if trackStats and pfUI.uf and pfUI.uf.stats and pfUI.uf.stats.enabled then + local lastStats = pfUI.api.lastUnitStats[unitstr] + if not lastStats or lastStats.hp ~= hp or lastStats.maxHp ~= maxHp or + lastStats.power ~= power or lastStats.maxPower ~= maxPower then + pfUI.uf.stats.nampowerUsed = (pfUI.uf.stats.nampowerUsed or 0) + 1 + pfUI.api.lastUnitStats[unitstr] = { + hp = hp, + maxHp = maxHp, + power = power, + maxPower = maxPower + } + end + end + + return hp, maxHp, power or 0, maxPower or 1, powerType + end + end + end + end + + -- Fallback to standard API (for players when Nampower fails) + hp = UnitHealth(unitstr) or 0 + maxHp = UnitHealthMax(unitstr) or 1 + powerType = UnitPowerType(unitstr) or 0 + power = UnitMana(unitstr) or 0 + maxPower = UnitManaMax(unitstr) or 1 + + -- Track Fallback usage - NUR bei echten Änderungen + if trackStats and not usedNampower then + if pfUI.uf and pfUI.uf.stats and pfUI.uf.stats.enabled then + local lastStats = pfUI.api.lastUnitStats[unitstr] + if not lastStats or lastStats.hp ~= hp or lastStats.maxHp ~= maxHp or + lastStats.power ~= power or lastStats.maxPower ~= maxPower then + pfUI.uf.stats.fallbackUsed = (pfUI.uf.stats.fallbackUsed or 0) + 1 + pfUI.api.lastUnitStats[unitstr] = { + hp = hp, + maxHp = maxHp, + power = power, + maxPower = maxPower + } + end + end + end + + return hp, maxHp, power, maxPower, powerType +end + +local aggrodata = { } +function pfUI.api.UnitHasAggro(unit) + -- Only cache positive results to allow instant detection when aggro changes + if aggrodata[unit] and aggrodata[unit].state > 0 and GetTime() < aggrodata[unit].check + 1 then + return aggrodata[unit].state + end + + aggrodata[unit] = aggrodata[unit] or { } + aggrodata[unit].check = GetTime() + aggrodata[unit].state = 0 + + if UnitExists(unit) and UnitIsFriend(unit, "player") then + for u in pairs(pfValidUnits) do + local t = u .. "target" + local tt = t .. "target" + + if UnitExists(t) and UnitIsUnit(t, unit) and UnitCanAttack(u, unit) then + aggrodata[unit].state = aggrodata[unit].state + 1 + end + + if UnitExists(tt) and UnitIsUnit(tt, unit) and UnitCanAttack(t, unit) then + aggrodata[unit].state = aggrodata[unit].state + 1 + end + end + end + + return aggrodata[unit].state +end + +pfUI.uf.glow = CreateFrame("Frame", nil, UIParent) +pfUI.uf.glow:SetScript("OnUpdate", function() + local fpsmod = GetFramerate() / 30 + if not this.val or this.val >= .8 then + this.mod = -0.01 / fpsmod + elseif this.val <= .4 then + this.mod = 0.01 / fpsmod + end + this.val = this.val + this.mod +end) + +pfUI.uf.glow.mod = 0 +pfUI.uf.glow.val = 0.6 + +function pfUI.uf.glow.UpdateGlowAnimation() + local val = pfUI.uf.glow.val or 0.6 + if val < 0.4 then val = 0.4 end + if val > 0.8 then val = 0.8 end + this:SetAlpha(val) +end + +local detect_icon, detect_name +local buff_icons_seeded = false +function pfUI.uf:DetectBuff(name, id) + if not name or not id then return end + + -- skip here if disabled + if pfUI_config.unitframes.buffdetect == "0" then + return UnitBuff(name, id) + end + + -- clear previously assigned + detect_icon, detect_name = nil, nil + + -- register tooltip scanner + scanner = scanner or libtipscan:GetScanner("unitframes") + + -- make sure the icon cache exists + pfUI_cache.buff_icons = pfUI_cache.buff_icons or {} + + -- seed cache from static locale data once per login + -- reverses L["icons"] (name→icon) into buff_icons (icon→name) + -- so that pfUI_cache.buff_icons[detect_icon] hits for all known buffs immediately + if not buff_icons_seeded then + for name, icon in pairs(L["icons"]) do + local path = "Interface\\Icons\\" .. icon + pfUI_cache.buff_icons[path] = pfUI_cache.buff_icons[path] or name + end + buff_icons_seeded = true + end + + -- check the regular way + detect_icon = UnitBuff(name, id) + if detect_icon then + if not pfUI_cache.buff_icons[detect_icon] then + -- read buff name and cache it + scanner:SetUnitBuff(name, id) + detect_name = scanner:Line(1) + + if detect_name then + pfUI_cache.buff_icons[detect_icon] = detect_name + end + end + + -- return the regular function + return UnitBuff(name, id) + end + + -- try to guess the buff based on tooltips and icon caches + scanner:SetUnitBuff(name, id) + detect_name = scanner:Line(1) + + if detect_name then + -- try to find the spell icon in locales + if L["icons"][detect_name] then + return "Interface\\Icons\\" .. L["icons"][detect_name], 1 + end + + -- try to find the spell icon in caches + for icon, name in pairs(pfUI_cache.buff_icons) do + if name == detect_name then return icon, 1 end + end + + -- try GetUnitField spellId -> libdebuff icon (Nampower, for custom spells) + if GetUnitField and GetUnitGUID and pfUI.libdebuff_GetSpellIcon then + local guid = GetUnitGUID(name) + if guid then + local auras = GetUnitField(guid, "aura") + if auras then + for _, spellId in pairs(auras) do + if type(spellId) == "number" and spellId > 0 then + local sname = GetSpellRecField and GetSpellRecField(spellId, "name") + if sname == detect_name then + local tex = pfUI.libdebuff_GetSpellIcon(spellId) + if tex and not string.find(tex, "QuestionMark") then + pfUI_cache.buff_icons[tex] = detect_name + return tex, 1 + end + end + end + end + end + end + end + + -- return fallback image + return "interface\\icons\\inv_misc_questionmark", 1 + end + + -- nothing found + return nil +end + +function pfUI.uf:UpdateVisibility() + local self = self or this + + -- we're infight, delay the update + if InCombatLockdown and InCombatLockdown() then + pfUI.uf.delayed[self] = true + return + end + + -- cache result of strsub to avoid repeating calls + if not self.cache_raid then + if strsub(self:GetName(),0,6) == "pfRaid" then + self.cache_raid = tonumber(strsub(self:GetName(),7,8)) + else + self.cache_raid = 0 + end + end + + -- show groupframes as raid + if self.cache_raid > 0 then + local id = self.cache_raid + + -- always show self in raidframes + if not UnitInRaid("player") and GetNumPartyMembers() == 0 and C.unitframes.selfinraid == "1" and id == 1 then + self.id = "" + self.label = "player" + + -- use raidframes for groups + elseif not UnitInRaid("player") and GetNumPartyMembers() > 0 and C.unitframes.raidforgroup == "1" then + if id == 1 then + self.id = "" + self.label = "player" + elseif id <= 5 then + self.id = id - 1 + self.label = "party" + end + + -- reset to regular raid unitstrings + elseif self.label == "party" or self.label == "player" then + self.id = id + self.label = "raid" + end + end + + -- display every unit as player while pfUI.uf.showall is set + if pfUI.uf.showall then + self._label = self._label or self.label + self._id = self._id or self.id + self.label, self.id = "player", "" + elseif not pfUI.uf.showall and self._label and self._id then + self.label, self.id = self._label, self._id + self._label, self._id = nil, nil + end + + local unitstr = string.format("%s%s", self.label or "", self.id or "") + local visibility = string.format("[target=%s,exists] show; hide", unitstr) + + if pfUI.unlock and pfUI.unlock:IsShown() then + -- display during unlock mode + visibility = "show" + self.visible = true + elseif self.config.visible == "0" then + -- frame shall not be visible + visibility = "hide" + self.visible = nil + elseif C["unitframes"]["group"]["hide_in_raid"] == "1" and self.label and strsub(self.label,0,5) == "party" and UnitInRaid("player") then + -- hide group while in raid and option is set + visibility = "hide" + self.visible = nil + elseif ( self.fname == "Group0" or self.fname == "PartyPet0" or self.fname == "Party0Target" ) + and (GetNumPartyMembers() <= 0 or (C["unitframes"]["group"]["hide_in_raid"] == "1" and UnitInRaid("player"))) then + -- hide self in group if solo or hide in raid is set + visibility = "hide" + self.visible = nil + end + + -- tbc visibility + if pfUI.client > 11200 then + self:SetAttribute("unit", unitstr) + + -- update visibility condition on change + if self.visibilitycondition ~= visibility then + RegisterStateDriver(self, 'visibility', visibility) + self.visibilitycondition = visibility + self.visible = true + end + + return + end + + -- vanilla visibility + if self.unitname and self.unitname ~= "focus" and self.unitname ~= "focustarget" then + self:Show() + elseif visibility == "hide" then + self:Hide() + elseif visibility == "show" then + self:Show() + else + if UnitName(unitstr) then + -- hide existing but too far away pet and pets of old group members + if self.label == "partypet" then + if not UnitIsVisible(unitstr) or not UnitExists("party" .. self.id) then + self:Hide() + return + end + elseif self.label == "pettarget" then + if not UnitIsVisible(unitstr) or not UnitExists("pet") then + self:Hide() + return + end + end + self:Show() + else + self.lastUnit = nil + self:Hide() + end + end +end + +function pfUI.uf:UpdateFrameSize() + local rawborder, default_border = GetBorderSize("unitframes") + local spacing = self.config.pspace * GetPerfectPixel() + local width = self.config.width + local height = self.config.height + local pheight = self.config.pheight + local ptwidth = self.config.portraitwidth + local ptheight = self.config.portraitheight + + local real_height = height + spacing + pheight + 2*default_border + if spacing ~= abs(spacing) and abs(spacing) > tonumber(pheight) then + real_height = height + spacing = 0 + end + + local portrait = 0 + + if self.config.portrait == "left" or self.config.portrait == "right" then + if ptwidth == "-1" and ptheight == "-1" then + -- align portrait size to frame + self.portrait:SetWidth(real_height) + self.portrait:SetHeight(real_height) + portrait = real_height + spacing + 2*default_border + else + -- use custom portrait size + self.portrait:SetWidth(ptwidth) + self.portrait:SetHeight(ptheight) + portrait = ptwidth + spacing + 2*default_border + end + end + + self:SetWidth(width + portrait) + self:SetHeight(real_height) +end + +function pfUI.uf:UpdateConfig() + local f = self + local C = pfUI_config + local rawborder, default_border = GetBorderSize("unitframes") + local spacing = f.config.pspace * GetPerfectPixel() + + local cooldown_text = tonumber(f.config.cooldown_text) + local cooldown_anim = tonumber(f.config.cooldown_anim) + + local relative_point = "BOTTOM" + if f.config.panchor == "TOPLEFT" then + relative_point = "BOTTOMLEFT" + elseif f.config.panchor == "TOPRIGHT" then + relative_point = "BOTTOMRIGHT" + end + + f.dispellable = nil + f.indicators = nil + f.indicator_custom = nil + + f.alpha_visible = tonumber(f.config.alpha_visible) + f.alpha_outrange = tonumber(f.config.alpha_outrange) + f.alpha_offline = tonumber(f.config.alpha_offline) + + f:SetFrameStrata("MEDIUM") + + f.glow:SetFrameStrata("BACKGROUND") + f.glow:SetFrameLevel(0) + f.glow:SetBackdrop(glow2) + f.glow:SetPoint("TOPLEFT", f, "TOPLEFT", -6 - default_border,6 + default_border) + f.glow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 6 + default_border,-6 - default_border) + f.glow:SetScript("OnUpdate", pfUI.uf.glow.UpdateGlowAnimation) + f.glow:Hide() + + f.combat:SetWidth(tonumber(f.config.squaresize)) + f.combat:SetHeight(tonumber(f.config.squaresize)) + f.combat:ClearAllPoints() + f.combat:SetPoint(f.config.squarepos, 0, 0) + f.combat:Hide() + + f.hp:ClearAllPoints() + f.hp:SetPoint("TOP", 0, 0) + + f.hp:SetWidth(f.config.width) + f.hp:SetHeight(f.config.height) + if tonumber(f.config.height) < 0 then f.hp:Hide() end + pfUI.api.CreateBackdrop(f.hp, default_border) + + f.hp.bar:SetStatusBarTexture(pfUI.media[f.config.bartexture]) + f.hp.bar:SetAllPoints(f.hp) + if f.config.verticalbar == "1" then + f.hp.bar:SetOrientation("VERTICAL") + else + f.hp.bar:SetOrientation("HORIZONTAL") + end + + local custombg = f.config.defcolor == "0" and f.config.custombg or C.unitframes.custombg + local custombgcolor = f.config.defcolor == "0" and f.config.custombgcolor or C.unitframes.custombgcolor + + if custombg == "1" then + local cr, cg, cb, ca = GetStringColor(custombgcolor) + cr, cg, cb, ca = tonumber(cr), tonumber(cg), tonumber(cb), tonumber(ca) + f.hp.bar:SetStatusBarBackgroundTexture(cr,cg,cb,ca) + end + + f.power:ClearAllPoints() + f.power:SetPoint(f.config.panchor, f.hp, relative_point, f.config.poffx, -2 * default_border - spacing + f.config.poffy * GetPerfectPixel()) + f.power:SetWidth((f.config.pwidth ~= "-1" and f.config.pwidth or f.config.width)) + f.power:SetHeight(f.config.pheight) + if tonumber(f.config.pheight) < 0 then f.power:Hide() end + + pfUI.api.CreateBackdrop(f.power, default_border) + f.power.bar:SetStatusBarTexture(pfUI.media[f.config.pbartexture]) + f.power.bar:SetAllPoints(f.power) + + local custompbg = f.config.defcolor == "0" and f.config.custompbg or C.unitframes.custompbg + local custompbgcolor = f.config.defcolor == "0" and f.config.custompbgcolor or C.unitframes.custompbgcolor + + if custompbg == "1" then + local cr, cg, cb, ca = GetStringColor(custompbgcolor) + cr, cg, cb, ca = tonumber(cr), tonumber(cg), tonumber(cb), tonumber(ca) + f.power.bar:SetStatusBarBackgroundTexture(cr,cg,cb,ca) + end + + local fontname, fontsize, fontstyle + if f.config.customfont == "1" then + fontname = pfUI.media[f.config.customfont_name] + fontsize = tonumber(f.config.customfont_size) + fontstyle = f.config.customfont_style + else + fontname = pfUI.font_unit + fontsize = tonumber(C.global.font_unit_size) + fontstyle = C.global.font_unit_style + end + + f.portrait.tex:SetAllPoints(f.portrait) + f.portrait.tex:SetTexCoord(.1, .9, .1, .9) + f.portrait.model:SetAllPoints(f.portrait) + + if f.config.portrait == "bar" then + f.portrait:SetParent(f.hp.bar) + f.portrait:SetAllPoints(f.hp.bar) + + f.portrait:SetAlpha(C.unitframes.portraitalpha) + if f.portrait.backdrop then f.portrait.backdrop:Hide() end + + -- place portrait below fonts + f.portrait.model:SetFrameLevel(3) + + f.portrait:Show() + elseif f.config.portrait == "left" then + f.portrait:SetParent(f) + f.portrait:ClearAllPoints() + f.portrait:SetPoint("LEFT", f, "LEFT", 0, 0) + + f.hp:ClearAllPoints() + f.hp:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) + + f.portrait:SetAlpha(f:GetAlpha()) + + -- make sure incHeal is above + f.portrait:SetFrameStrata("BACKGROUND") + f.portrait.model:SetFrameStrata("BACKGROUND") + f.portrait.model:SetFrameLevel(1) + + pfUI.api.CreateBackdrop(f.portrait, default_border) + f.portrait.backdrop:Show() + f.portrait:Show() + elseif f.config.portrait == "right" then + f.portrait:SetParent(f) + f.portrait:ClearAllPoints() + f.portrait:SetPoint("RIGHT", f, "RIGHT", 0, 0) + + f.hp:ClearAllPoints() + f.hp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + + f.portrait:SetAlpha(f:GetAlpha()) + + -- make sure incHeal is above + f.portrait:SetFrameStrata("BACKGROUND") + f.portrait.model:SetFrameStrata("BACKGROUND") + f.portrait.model:SetFrameLevel(1) + + pfUI.api.CreateBackdrop(f.portrait, default_border) + f.portrait.backdrop:Show() + f.portrait:Show() + else + f.portrait:Hide() + end + + if f.group then + if f.config.raidgrouplabel == "1" then + f.group:Show() + else + f.group:Hide() + end + + local xoff = tonumber(f.config.grouplabelxoff) or 0 + local yoff = tonumber(f.config.grouplabelyoff) or 8 + f.group:SetPoint("TOPLEFT", f, "BOTTOMLEFT", xoff, yoff) + f.group:SetPoint("TOPRIGHT", f, "BOTTOMRIGHT", xoff, yoff) + end + + if f.config.hitindicator == "1" then + f.feedbackText:SetFont(pfUI.media[f.config.hitindicatorfont], f.config.hitindicatorsize, "OUTLINE") + f.feedbackFontHeight = f.config.hitindicatorsize + f.feedbackStartTime = GetTime() + if f.config.portrait == "bar" or f.config.portrait == "off" then + f.feedbackText:SetParent(f.texts) + f.feedbackText:ClearAllPoints() + f.feedbackText:SetPoint("CENTER", f.hp.bar, "CENTER") + else + f.feedbackText:SetParent(f.portrait) + f.feedbackText:ClearAllPoints() + f.feedbackText:SetPoint("CENTER", f.portrait, "CENTER") + end + f:RegisterEvent("UNIT_COMBAT") + else + f.feedbackText:Hide() + f:UnregisterEvent("UNIT_COMBAT") + end + + f.hpLeftText:SetFontObject(GameFontWhite) + f.hpLeftText:SetFont(fontname, fontsize, fontstyle) + f.hpLeftText:SetJustifyH("LEFT") + f.hpLeftText:ClearAllPoints() + f.hpLeftText:SetPoint("TOPLEFT",f.hp.bar, "TOPLEFT", 2*(default_border + f.config.txthpleftoffx), 1 + tonumber(f.config.txthpleftoffy)) + f.hpLeftText:SetPoint("BOTTOMRIGHT",f.hp.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txthpleftoffx), f.config.txthpleftoffy) + + f.hpRightText:SetFontObject(GameFontWhite) + f.hpRightText:SetFont(fontname, fontsize, fontstyle) + f.hpRightText:SetJustifyH("RIGHT") + f.hpRightText:ClearAllPoints() + f.hpRightText:SetPoint("TOPLEFT",f.hp.bar, "TOPLEFT", 2*(default_border + f.config.txthprightoffx), 1 + tonumber(f.config.txthprightoffy)) + f.hpRightText:SetPoint("BOTTOMRIGHT",f.hp.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txthprightoffx), f.config.txthprightoffy) + + f.hpCenterText:SetFontObject(GameFontWhite) + f.hpCenterText:SetFont(fontname, fontsize, fontstyle) + f.hpCenterText:SetJustifyH("CENTER") + f.hpCenterText:ClearAllPoints() + f.hpCenterText:SetPoint("TOPLEFT",f.hp.bar, "TOPLEFT", f.config.txthpcenteroffx, 1 + tonumber(f.config.txthpcenteroffy)) + f.hpCenterText:SetPoint("BOTTOMRIGHT",f.hp.bar, "BOTTOMRIGHT", f.config.txthpcenteroffx, f.config.txthpcenteroffy) + + f.powerLeftText:SetFontObject(GameFontWhite) + f.powerLeftText:SetFont(fontname, fontsize, fontstyle) + f.powerLeftText:SetJustifyH("LEFT") + f.powerLeftText:ClearAllPoints() + f.powerLeftText:SetPoint("TOPLEFT",f.power.bar, "TOPLEFT", 2*(default_border + f.config.txtpowerleftoffx), 1 + tonumber(f.config.txtpowerleftoffy)) + f.powerLeftText:SetPoint("BOTTOMRIGHT",f.power.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txtpowerleftoffx), f.config.txtpowerleftoffy) + + f.powerRightText:SetFontObject(GameFontWhite) + f.powerRightText:SetFont(fontname, fontsize, fontstyle) + f.powerRightText:SetJustifyH("RIGHT") + f.powerRightText:ClearAllPoints() + f.powerRightText:SetPoint("TOPLEFT",f.power.bar, "TOPLEFT", 2*(default_border + f.config.txtpowerrightoffx), 1 + tonumber(f.config.txtpowerrightoffy)) + f.powerRightText:SetPoint("BOTTOMRIGHT",f.power.bar, "BOTTOMRIGHT", -2*(default_border + f.config.txtpowerrightoffx), f.config.txtpowerrightoffy) + + f.powerCenterText:SetFontObject(GameFontWhite) + f.powerCenterText:SetFont(fontname, fontsize, fontstyle) + f.powerCenterText:SetJustifyH("CENTER") + f.powerCenterText:ClearAllPoints() + f.powerCenterText:SetPoint("TOPLEFT",f.power.bar, "TOPLEFT", f.config.txtpowercenteroffx, 1 + tonumber(f.config.txtpowercenteroffy)) + f.powerCenterText:SetPoint("BOTTOMRIGHT",f.power.bar, "BOTTOMRIGHT", f.config.txtpowercenteroffx, f.config.txtpowercenteroffy) + + f.incHeal:SetHeight(f.config.height) + f.incHeal:SetWidth(f.config.width) + f.incHeal.texture:SetTexture(pfUI.media["img:bar"]) + local cr, cg, cb, ca = GetStringColor(f.config.healcolor) + cr, cg, cb, ca = tonumber(cr), tonumber(cg), tonumber(cb), tonumber(ca) + f.incHeal.texture:SetVertexColor(cr, cg, cb, ca) + f.incHeal:Hide() + + if f.config.verticalbar == "0" then + f.incHeal:ClearAllPoints() + f.incHeal:SetPoint("TOPLEFT", f.hp.bar, "TOPLEFT", 0, 0) + else + f.incHeal:ClearAllPoints() + f.incHeal:SetPoint("BOTTOM", f.hp.bar, "BOTTOM", 0, 0) + end + + f.ressIcon:SetFrameLevel(16) + f.ressIcon:SetWidth(32) + f.ressIcon:SetHeight(32) + f.ressIcon:SetPoint("CENTER", f, "CENTER", 0, 4) + f.ressIcon.texture:SetTexture(pfUI.media["img:ress"]) + f.ressIcon.texture:SetAllPoints(f.ressIcon) + f.ressIcon:Hide() + + f.leaderIcon:SetWidth(10) + f.leaderIcon:SetHeight(10) + f.leaderIcon:SetPoint("CENTER", f, "TOPLEFT", 0, 0) + f.leaderIcon.texture:SetTexture("Interface\\GROUPFRAME\\UI-Group-LeaderIcon") + f.leaderIcon.texture:SetAllPoints(f.leaderIcon) + f.leaderIcon:Hide() + + f.lootIcon:SetWidth(10) + f.lootIcon:SetHeight(10) + f.lootIcon:SetPoint("CENTER", f, "LEFT", 0, 0) + f.lootIcon.texture:SetTexture("Interface\\GROUPFRAME\\UI-Group-MasterLooter") + f.lootIcon.texture:SetAllPoints(f.lootIcon) + f.lootIcon:Hide() + + f.pvpIcon:SetWidth(f.config.pvpiconsize) + f.pvpIcon:SetHeight(f.config.pvpiconsize) + f.pvpIcon:SetPoint(f.config.pvpiconalign, f, f.config.pvpiconalign, f.config.pvpiconoffx, f.config.pvpiconoffy) + f.pvpIcon.texture:SetTexture(pfUI.media["img:pvp"]) + f.pvpIcon.texture:SetAllPoints(f.pvpIcon) + f.pvpIcon.texture:SetVertexColor(1,1,1,.5) + f.pvpIcon:Hide() + + f.raidIcon:SetWidth(f.config.raidiconsize) + f.raidIcon:SetHeight(f.config.raidiconsize) + f.raidIcon:SetPoint("CENTER", f, f.config.raidiconalign, f.config.raidiconoffx, f.config.raidiconoffy) + local raidIconTex = C.unitframes.blizzard_raidicons == "1" and "Interface\\TargetingFrame\\UI-RaidTargetingIcons" or pfUI.media["img:raidicons"] + f.raidIcon.texture:SetTexture(raidIconTex) + f.raidIcon.texture:SetAllPoints(f.raidIcon) + f.raidIcon:Hide() + + f.restIcon:SetWidth(16) + f.restIcon:SetHeight(16) + f.restIcon:SetPoint("TOP", f, "TOPLEFT", 0, -1) + f.restIcon.texture:SetTexture("Interface\\CharacterFrame\\UI-StateIcon", true) + f.restIcon.texture:SetTexCoord(0, .5, 0, .421875) + f.restIcon.texture:SetAllPoints(f.restIcon) + f.restIcon:Hide() + + f.happinessIcon:SetWidth(tonumber(C.unitframes.pet.happinesssize)) + f.happinessIcon:SetHeight(tonumber(C.unitframes.pet.happinesssize)) + f.happinessIcon:SetPoint("CENTER", f, "TOPLEFT", default_border, -default_border) + f.happinessIcon.texture:SetTexture(pfUI.media["img:neutral"]) + f.happinessIcon.texture:SetAllPoints(f.happinessIcon) + f.happinessIcon.texture:SetVertexColor(1, 1, 0, 1) + f.happinessIcon:Hide() + + if f.config.buffs == "off" then + for i=1, 32 do + if f.buffs and f.buffs[i] then + f.buffs[i]:Hide() + f.buffs[i] = nil + end + end + f.buffs = nil + else + f.buffs = f.buffs or {} + + for i=1, 32 do + if i > tonumber(f.config.bufflimit) then break end + + local perrow = f.config.buffperrow + local row = floor((i-1) / perrow) + + f.buffs[i] = f.buffs[i] or CreateFrame("Button", "pfUI" .. f.fname .. "Buff" .. i, f) + f.buffs[i].texture = f.buffs[i].texture or f.buffs[i]:CreateTexture() + f.buffs[i].texture:SetTexCoord(.08, .92, .08, .92) + f.buffs[i].texture:SetAllPoints() + f.buffs[i].stacks = f.buffs[i].stacks or f.buffs[i]:CreateFontString(nil, "OVERLAY", f.buffs[i]) + f.buffs[i].stacks:SetFont(pfUI.font_unit, C.global.font_unit_size, "OUTLINE") + f.buffs[i].stacks:SetPoint("BOTTOMRIGHT", f.buffs[i], 2, -2) + f.buffs[i].stacks:SetJustifyH("LEFT") + f.buffs[i].stacks:SetShadowColor(0, 0, 0) + f.buffs[i].stacks:SetShadowOffset(0.8, -0.8) + f.buffs[i].stacks:SetTextColor(1,1,.5) + + f.buffs[i]:SetFrameLevel(12) + f.buffs[i]:RegisterForClicks("RightButtonUp") + f.buffs[i]:ClearAllPoints() + + local invert_h, invert_v, af + if f.config.buffs == "TOPLEFT" then + invert_h = 1 + invert_v = 1 + af = "BOTTOMLEFT" + elseif f.config.buffs == "BOTTOMLEFT" then + invert_h = -1 + invert_v = 1 + af = "TOPLEFT" + elseif f.config.buffs == "TOPRIGHT" then + invert_h = 1 + invert_v = -1 + af = "BOTTOMRIGHT" + elseif f.config.buffs == "BOTTOMRIGHT" then + invert_h = -1 + invert_v = -1 + af = "TOPRIGHT" + end + + local anchor = f.config.portraitheight ~= "-1" and f.hp or f + if anchor == f.hp and (f.config.buffs == "BOTTOMLEFT" or f.config.buffs == "BOTTOMRIGHT") then + anchor = f.power + end + local multiply = C.appearance.border.force_blizz == "1" and 1 or 2 + f.buffs[i]:SetPoint(af, anchor, f.config.buffs, + invert_v * (i-1-row*perrow)*(multiply*default_border + f.config.buffsize + 1), + invert_h * (row*(multiply*default_border + f.config.buffsize + 1) + (multiply*default_border + 1))) + + f.buffs[i]:SetWidth(f.config.buffsize) + f.buffs[i]:SetHeight(f.config.buffsize) + + -- Create CD frame if it doesn't exist + if not f.buffs[i].cd then + if cooldown_anim == 1 then + -- Animation enabled: Use Model frame with CooldownFrameTemplate + f.buffs[i].cd = CreateFrame(COOLDOWN_FRAME_TYPE, f.buffs[i]:GetName() .. "Cooldown", f.buffs[i], "CooldownFrameTemplate") + else + -- Animation disabled: Use regular Frame with dummy functions + f.buffs[i].cd = CreateFrame("Frame", f.buffs[i]:GetName() .. "Cooldown", f.buffs[i]) + f.buffs[i].cd.AdvanceTime = DoNothing + f.buffs[i].cd.SetSequence = DoNothing + f.buffs[i].cd.SetSequenceTime = DoNothing + end + end + + -- Always update CD properties (in case size changed) + local cdScale = f.config.buffsize / 32 + f.buffs[i].cd:ClearAllPoints() + f.buffs[i].cd:SetScale(cdScale) + f.buffs[i].cd:SetAllPoints(f.buffs[i]) + f.buffs[i].cd:SetFrameLevel(14) + f.buffs[i].cd.pfCooldownType = "ALL" + f.buffs[i].cd.pfCooldownStyleText = cooldown_text + f.buffs[i].cd.pfCooldownStyleAnimation = cooldown_anim + f.buffs[i].cd:SetAlpha(cooldown_anim == 1 and 1 or 0) + + -- immediately show/hide existing cooldown text + if f.buffs[i].cd.pfCooldownText then + if cooldown_text == 1 then + f.buffs[i].cd.pfCooldownText:Show() + else + f.buffs[i].cd.pfCooldownText:Hide() + end + end + + f.buffs[i].id = i + f.buffs[i]:Hide() + + CreateBackdrop(f.buffs[i], default_border) + + if f:GetName() == "pfPlayer" then + f.buffs[i]:SetScript("OnUpdate", BuffOnUpdate) + elseif f:GetName() == "pfTarget" and pfUI.expansion == "tbc" then + f.buffs[i]:SetScript("OnUpdate", TargetBuffOnUpdate) + end + + f.buffs[i]:SetScript("OnEnter", BuffOnEnter) + f.buffs[i]:SetScript("OnLeave", BuffOnLeave) + f.buffs[i]:SetScript("OnClick", BuffOnClick) + end + end + + if f.config.debuffs == "off" then + for i=1, 32 do + if f.debuffs and f.debuffs[i] then + f.debuffs[i]:Hide() + f.debuffs[i] = nil + end + end + f.debuffs = nil + else + f.debuffs = f.debuffs or {} + + for i=1, 32 do + if i > tonumber(f.config.debufflimit) then break end + + f.debuffs[i] = f.debuffs[i] or CreateFrame("Button", "pfUI" .. f.fname .. "Debuff" .. i, f) + f.debuffs[i].texture = f.debuffs[i].texture or f.debuffs[i]:CreateTexture() + f.debuffs[i].texture:SetTexCoord(.08, .92, .08, .92) + f.debuffs[i].texture:SetAllPoints() + f.debuffs[i].stacks = f.debuffs[i].stacks or f.debuffs[i]:CreateFontString(nil, "OVERLAY", f.debuffs[i]) + f.debuffs[i].stacks:SetFont(pfUI.font_unit, C.global.font_unit_size, "OUTLINE") + f.debuffs[i].stacks:SetPoint("BOTTOMRIGHT", f.debuffs[i], 2, -2) + f.debuffs[i].stacks:SetJustifyH("LEFT") + f.debuffs[i].stacks:SetShadowColor(0, 0, 0) + f.debuffs[i].stacks:SetShadowOffset(0.8, -0.8) + f.debuffs[i].stacks:SetTextColor(1,1,.5) + + f.debuffs[i]:SetFrameLevel(12) + f.debuffs[i]:RegisterForClicks("RightButtonUp") + f.debuffs[i]:ClearAllPoints() + f.debuffs[i]:SetWidth(f.config.debuffsize) + f.debuffs[i]:SetHeight(f.config.debuffsize) + f.debuffs[i]:SetNormalTexture(nil) + + -- Create CD frame if it doesn't exist + if not f.debuffs[i].cd then + if cooldown_anim == 1 then + -- Animation enabled: Use Model frame with CooldownFrameTemplate + f.debuffs[i].cd = CreateFrame(COOLDOWN_FRAME_TYPE, f.debuffs[i]:GetName() .. "Cooldown", f.debuffs[i], "CooldownFrameTemplate") + else + -- Animation disabled: Use regular Frame with dummy functions + f.debuffs[i].cd = CreateFrame("Frame", f.debuffs[i]:GetName() .. "Cooldown", f.debuffs[i]) + f.debuffs[i].cd.AdvanceTime = DoNothing + f.debuffs[i].cd.SetSequence = DoNothing + f.debuffs[i].cd.SetSequenceTime = DoNothing + end + end + + -- Always update CD properties (in case size changed) + local cdScale = f.config.debuffsize / 32 + f.debuffs[i].cd:ClearAllPoints() + f.debuffs[i].cd:SetScale(cdScale) + f.debuffs[i].cd:SetAllPoints(f.debuffs[i]) + f.debuffs[i].cd:SetFrameLevel(14) + f.debuffs[i].cd.pfCooldownType = "ALL" + f.debuffs[i].cd.pfCooldownStyleText = cooldown_text + f.debuffs[i].cd.pfCooldownStyleAnimation = cooldown_anim + f.debuffs[i].cd:SetAlpha(cooldown_anim == 1 and 1 or 0) + + -- immediately show/hide existing cooldown text + if f.debuffs[i].cd.pfCooldownText then + if cooldown_text == 1 then + f.debuffs[i].cd.pfCooldownText:Show() + else + f.debuffs[i].cd.pfCooldownText:Hide() + end + end + + f.debuffs[i].id = i + f.debuffs[i]:Hide() + + CreateBackdrop(f.debuffs[i], default_border) + + if f:GetName() == "pfPlayer" then + f.debuffs[i]:SetScript("OnUpdate", DebuffOnUpdate) + end + + f.debuffs[i]:SetScript("OnEnter", DebuffOnEnter) + f.debuffs[i]:SetScript("OnLeave", DebuffOnLeave) + f.debuffs[i]:SetScript("OnClick", DebuffOnClick) + end + end + + if f.config.visible == "1" then + pfUI.uf:RefreshUnit(f, "all") + f:EnableScripts() + f:EnableEvents() + f:UpdateFrameSize() + else + f:UnregisterAllEvents() + f:Hide() + end +end + +function pfUI.uf.OnShow() + pfUI.uf:RefreshUnit(this, "portrait") + pfUI.uf:RefreshUnit(this, "base") +end + +function pfUI.uf.OnEvent() + -- Handle shutdown to prevent crash 132 + if event == "PLAYER_LOGOUT" then + this:UnregisterAllEvents() + this:SetScript("OnEvent", nil) + this:SetScript("OnUpdate", nil) + return + end + + -- update indicators + if event == "PARTY_LEADER_CHANGED" or + event == "PARTY_LOOT_METHOD_CHANGED" or + event == "PARTY_MEMBERS_CHANGED" or + event == "RAID_TARGET_UPDATE" or + event == "RAID_ROSTER_UPDATE" or + event == "PLAYER_UPDATE_RESTING" + then + this.update_indicators = true + end + + -- abort on broken unitframes (e.g focus) + if not this.label then return end + + -- update regular frames + if event == "PLAYER_ENTERING_WORLD" then + this.update_full = true + -- Clear GUID tracking on zone change for full rebuild + if pfUI.uf.ClearGuidTracking then pfUI.uf.ClearGuidTracking() end + elseif this.label == "target" and event == "PLAYER_TARGET_CHANGED" and not pfScanActive == true then + this.update_full = true + -- immediately update raid icon to avoid delay on target swap + if this.raidIcon and this.config and this.config.raidicon == "1" then + local raidIcon = UnitName("target") and GetRaidTargetIndex("target") + if raidIcon then + SetRaidTargetIconTexture(this.raidIcon.texture, raidIcon) + this.raidIcon:Show() + else + this.raidIcon:Hide() + end + end + elseif ( this.label == "raid" or this.label == "party" or this.label == "player" ) and event == "PARTY_MEMBERS_CHANGED" then + -- Smart update: check if THIS frame's unit actually changed + if pfUI.uf.guidTracker and this.id then + local unit = this.label == "player" and "player" or (this.label .. this.id) + local newGuid = GetUnitGUID(unit) + local oldGuid = pfUI.uf.guidTracker.frameToGuid[this] + if newGuid ~= oldGuid then + pfUI.uf.guidTracker.frameToGuid[this] = newGuid + this.update_full = true + end + else + this.update_full = true + end + elseif ( this.label == "raid" or this.label == "party" ) and event == "PARTY_MEMBER_ENABLE" then + this.update_full = true + elseif ( this.label == "raid" or this.label == "party" ) and event == "PARTY_MEMBER_DISABLE" then + this.update_full = true + elseif ( this.label == "raid" or this.label == "party" ) and event == "RAID_ROSTER_UPDATE" then + -- Note: Smart GUID-based updates are handled in raid.lua OnUpdate + -- after frame IDs are reassigned. We don't set update_full here anymore + -- for raid frames to avoid the freeze. + if this.label == "party" then + -- Party frames still need the old logic (no smart tracking yet) + this.update_full = true + end + -- Raid frames: update_full is set by raid.lua GUID tracker + elseif this.label == "pet" and event == "UNIT_PET" then + this.update_full = true + elseif this.label == "player" and (event == "PLAYER_AURAS_CHANGED" or event == "UNIT_INVENTORY_CHANGED") then + this.update_aura = true + elseif this.label == "pet" and event == "UNIT_HAPPINESS" then + this.update_full = true + -- UNIT_XXX Events + elseif arg1 and arg1 == this.label .. this.id then + this.lastEventUpdate = GetTime() + + if event == "UNIT_PORTRAIT_UPDATE" or event == "UNIT_MODEL_CHANGED" then + this.update_portrait = true + elseif event == "UNIT_AURA" then + this.update_aura = true + elseif event == "UNIT_FACTION" then + this.update_pvp = true + elseif event == "UNIT_COMBAT" then + CombatFeedback_OnCombatEvent(arg2, arg3, arg4, arg5) + else + this.update_base = true + end + end +end + +-- Local reference for performance +local _GetTime = GetTime + +-- Global cached time for libpredict and other functions +pfUI.uf.now = 0 + +-- ============================================================================ +-- GLOBAL FALLBACK THROTTLE - Limits total fallback updates across ALL frames +-- ============================================================================ +pfUI.uf.fallbackThrottle = { + lastUpdate = 0, + interval = 0.1, -- 10 updates per second total (not per frame!) + updatesThisInterval = 0, + maxUpdatesPerInterval = 5 -- Max 5 frames can update per interval +} + +-- ============================================================================ +-- STATS SYSTEM - Performance tracking for Nampower vs Fallback +-- ============================================================================ +pfUI.uf.stats = { + eventUpdates = 0, + heartbeatUpdates = 0, + earlyReturns = 0, + nampowerUsed = 0, + fallbackUsed = 0, + throttledSkips = 0, + startTime = 0, + enabled = true +} + +-- Stats Frame (Live Display) +pfUI.uf.statsFrame = CreateFrame("Frame", "pfUIStatsFrame", UIParent) +pfUI.uf.statsFrame:SetWidth(200) +pfUI.uf.statsFrame:SetHeight(220) +pfUI.uf.statsFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -10, -200) +pfUI.uf.statsFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 8, + insets = { left = 3, right = 3, top = 3, bottom = 3 } +}) +pfUI.uf.statsFrame:SetBackdropColor(0, 0, 0, 0.8) +pfUI.uf.statsFrame:EnableMouse(true) +pfUI.uf.statsFrame:SetMovable(true) +pfUI.uf.statsFrame:SetClampedToScreen(true) +pfUI.uf.statsFrame:RegisterForDrag("LeftButton") +pfUI.uf.statsFrame:SetScript("OnDragStart", function() this:StartMoving() end) +pfUI.uf.statsFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) +pfUI.uf.statsFrame:Hide() + +-- Stats Title +pfUI.uf.statsFrame.title = pfUI.uf.statsFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +pfUI.uf.statsFrame.title:SetPoint("TOP", pfUI.uf.statsFrame, "TOP", 0, -8) +pfUI.uf.statsFrame.title:SetText("Performance") + +-- Stats Text (multi-line) +pfUI.uf.statsFrame.text = pfUI.uf.statsFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") +pfUI.uf.statsFrame.text:SetPoint("TOPLEFT", pfUI.uf.statsFrame, "TOPLEFT", 10, -30) +pfUI.uf.statsFrame.text:SetWidth(180) +pfUI.uf.statsFrame.text:SetHeight(180) +pfUI.uf.statsFrame.text:SetJustifyH("LEFT") +pfUI.uf.statsFrame.text:SetJustifyV("TOP") +pfUI.uf.statsFrame.text:SetText("Initializing...") + +-- Update function for stats display +pfUI.uf.UpdateStatsDisplay = function() + local elapsed = GetTime() - pfUI.uf.stats.startTime + if elapsed < 0.1 then return end + + local eventRate = pfUI.uf.stats.eventUpdates / elapsed + local heartbeatRate = pfUI.uf.stats.heartbeatUpdates / elapsed + local totalFrameUpdates = eventRate + heartbeatRate + + -- Calculate Nampower vs Fallback percentages (ONLY counts actual data changes!) + local totalDataChanges = pfUI.uf.stats.nampowerUsed + pfUI.uf.stats.fallbackUsed + local nampowerPct = totalDataChanges > 0 and math.floor((pfUI.uf.stats.nampowerUsed / totalDataChanges) * 100) or 0 + local fallbackPct = totalDataChanges > 0 and math.floor((pfUI.uf.stats.fallbackUsed / totalDataChanges) * 100) or 0 + + -- Calculate data change rate (how often HP/Mana actually changes) + local dataChangeRate = totalDataChanges / elapsed + + local statsText = string.format( + "Time: %.1fs\n" .. + "|cffaaaaaa--- Frame Updates ---|r\n" .. + "Event: %.1f/s (%d)\n" .. + "Heartbeat: %.1f/s (%d)\n" .. + "Total: %.1f/s\n" .. + "\n" .. + "|cffaaaaaa--- Data Changes ---|r\n" .. + "Rate: %.1f/s (%d)\n" .. + "|cff00ff00NP: %d%% (%d)|r\n" .. + "|cffff8800FB: %d%% (%d)|r", + elapsed, + eventRate, + pfUI.uf.stats.eventUpdates, + heartbeatRate, + pfUI.uf.stats.heartbeatUpdates, + totalFrameUpdates, + dataChangeRate, + totalDataChanges, + nampowerPct, + pfUI.uf.stats.nampowerUsed, + fallbackPct, + pfUI.uf.stats.fallbackUsed + ) + + pfUI.uf.statsFrame.text:SetText(statsText) +end + +-- Stats update timer +pfUI.uf.statsUpdateTimer = 0 + +-- Cache cleanup timer (clean lastUnitStats every 30s to prevent memory leak) +pfUI.uf.cacheCleanupTimer = 0 + +-- ============================================================================ +-- OnUpdate with Heartbeat Polling and Fallback +-- ============================================================================ +function pfUI.uf.OnUpdate() + local now = _GetTime() + pfUI.uf.now = now + + -- Update stats display (throttled to 0.2s) + if pfUI.uf.statsFrame and pfUI.uf.statsFrame:IsShown() then + if (pfUI.uf.statsUpdateTimer or 0) <= now then + pfUI.uf.statsUpdateTimer = now + 0.2 + if pfUI.uf.stats.startTime > 0 then + pfUI.uf.UpdateStatsDisplay() + end + end + end + + -- Cleanup lastUnitStats cache every 30 seconds to prevent memory leak + if (pfUI.uf.cacheCleanupTimer or 0) <= now then + pfUI.uf.cacheCleanupTimer = now + 30 + + -- Only keep cache for units that currently exist + if pfUI.api.lastUnitStats then + for unitstr in pairs(pfUI.api.lastUnitStats) do + if not _G.UnitExists(unitstr) then + pfUI.api.lastUnitStats[unitstr] = nil + end + end + end + end + + -- update combat feedback (no throttle - needs immediate feedback) + if this.feedbackText then CombatFeedback_OnUpdate(arg1) end + + -- Throttle raid/party frames for performance + if this.label == "raid" or this.label == "party" then + if (this.throttleTick or 0) > now then + if pfUI.uf.stats and pfUI.uf.stats.enabled then + pfUI.uf.stats.throttledSkips = pfUI.uf.stats.throttledSkips + 1 + end + return + end + this.throttleTick = now + 0.1 -- Default: 10 FPS + end + + -- ============================================================================ + -- EVENTLESS ACTIONS (Range Check, Online/Offline, Aggro) - MUST RUN ALWAYS + -- These run on their own timer, independent of event-based updates + -- ============================================================================ + if this.label then + -- Combat/Aggro Indicators (throttled to 0.2s) + if not this.lastCombatCheck then this.lastCombatCheck = now + 0.2 end + if this.lastCombatCheck < now then + this.lastCombatCheck = now + 0.2 + + if this.config and this.config.squareaggro == "1" and pfUI.api.UnitHasAggro(this.label .. this.id) > 0 then + this.combat.tex:SetTexture(1,.2,0) + this.combat:Show() + elseif this.config and this.config.squarecombat == "1" and UnitAffectingCombat(this.label .. this.id) then + this.combat.tex:SetTexture(1,1,.2) + this.combat:Show() + elseif this.combat then + this.combat:Hide() + end + end + + -- Range Check / Online-Offline State (throttled) + -- Validate tick value - it should be an interval (e.g. 0.5), not a timestamp + local tickInterval = this.tick + if tickInterval and tickInterval > 10 then + -- tick is a timestamp, not an interval - ignore it + tickInterval = nil + end + -- Reset lastTick if it's invalid (much larger than now, e.g. from corrupted state) + if this.lastTick and this.lastTick > now + 10 then + this.lastTick = nil + end + if not this.lastTick then this.lastTick = now + (tickInterval or .5) end + if this.lastTick < now then + local unitstr = this.label .. this.id + this.lastTick = now + (tickInterval or .5) + + -- target target has a huge delay, make sure to not tick during range checks + if this.label == "targettarget" or this.label == "targettargettarget" then + local name = UnitName(this.label) + if name ~= this.namebuf1 then + this.namebuf1 = name + elseif name ~= this.namebuf2 then + this.namebuf2 = name + else + pfUI.uf:RefreshUnitState(this) + pfUI.uf:RefreshIndicators(this) + end + else + pfUI.uf:RefreshUnitState(this) + pfUI.uf:RefreshIndicators(this) + end + + if this.config and this.config.glowaggro == "1" and pfUI.api.UnitHasAggro(unitstr) > 0 then + this.glow:SetBackdropBorderColor(1,.2,0) + this.glow:Show() + elseif this.config and this.config.glowcombat == "1" and UnitAffectingCombat(unitstr) then + this.glow:SetBackdropBorderColor(1,1,.2) + this.glow:Show() + elseif this.glow then + this.glow:Hide() + end + + -- update everything on eventless frames (targettarget, etc) + if this.tick then + pfUI.uf:RefreshUnit(this, "all") + end + end + + -- Heal Prediction (throttled to 0.1s for responsiveness) + if libpredict and this.incHeal then + if not this.lastHealTick then this.lastHealTick = now end + if this.lastHealTick < now then + this.lastHealTick = now + 0.1 + + local unit = this.label .. this.id + local heal = libpredict:UnitGetIncomingHeals(unit) + + -- O(1) Nampower lookup via GUID (same pattern as nameplates.lua) + local health, maxHealth + if GetUnitField then + local guid = GetUnitGUID(unit) + if guid then + health = GetUnitField(guid, "health") + maxHealth = GetUnitField(guid, "maxHealth") + end + end + -- Fallback to standard API + if not health or not maxHealth or maxHealth == 0 then + health, maxHealth = UnitHealth(unit), UnitHealthMax(unit) + end + + if heal - health - maxHealth ~= this.predictstate then + local overhealperc = tonumber(this.config.overhealperc) + this.predictstate = heal - health - maxHealth + + if heal > 0 and (health < maxHealth or overhealperc > 0 ) then + local width = this.config.width + local height = this.config.height + + if this.config.verticalbar == "0" then + local healthWidth = width * (health / maxHealth) + local incWidth = width * heal / maxHealth + if healthWidth + incWidth > width * (1+(overhealperc/100)) then + incWidth = width * (1+overhealperc/100) - healthWidth + end + + if this.config.invert_healthbar == "1" then + this.incHeal:SetWidth(incWidth) + else + this.incHeal:SetWidth(incWidth + healthWidth) + end + else + local healthHeight = height * (health / maxHealth) + local incHeight = height * heal / maxHealth + if healthHeight + incHeight > height * (1+(overhealperc/100)) then + incHeight = height * (1+overhealperc/100) - healthHeight + end + + if this.config.invert_healthbar == "1" then + this.incHeal:SetHeight(incHeight) + else + this.incHeal:SetHeight(incHeight + healthHeight) + end + end + + this.incHeal:Show() + else + this.incHeal:Hide() + end + end + + -- update ressurections + local ress = libpredict:UnitHasIncomingResurrection(unit) + if ress and UnitIsDeadOrGhost(unit) then + this.ressIcon:Show() + else + this.ressIcon:Hide() + end + end + end + end + + -- ============================================================================ + -- EVENT-BASED UPDATES (Health, Mana, Auras, etc.) + -- ============================================================================ + + -- Check if we have pending updates from events + local hasUpdates = this.update_full or this.update_base or + this.update_aura or this.update_portrait or + this.update_pvp or this.update_indicators + + -- Track event-triggered updates (not API calls, just frame updates) + if hasUpdates and pfUI.uf.stats and pfUI.uf.stats.enabled then + pfUI.uf.stats.eventUpdates = pfUI.uf.stats.eventUpdates + 1 + end + + -- Heartbeat Polling: If no events pending, check if we need fallback + if not hasUpdates then + local timeSinceEvent = this.lastEventUpdate and (now - this.lastEventUpdate) or 999 + + -- If >0.5s since last event and unit exists, try heartbeat + if timeSinceEvent > 0.5 and this.label and _G.UnitExists(this.label .. this.id) then + local needsFallback = false + + -- Check if Nampower can provide data + if GetUnitField then + -- Use _G.UnitExists to avoid conflicts with range checking + local unitstr = this.label .. this.id + local exists = _G.UnitExists(unitstr) + if exists then + local _, guid = _G.UnitExists(unitstr) + if guid then + local hp = GetUnitField(guid, "health") + if not hp or hp == 0 then + needsFallback = true + end + else + needsFallback = true + end + else + needsFallback = true + end + else + needsFallback = true + end + + if needsFallback then + -- GLOBAL Throttle: Limit fallback updates across ALL frames + local throttle = pfUI.uf.fallbackThrottle + + -- Reset counter each interval + if now - throttle.lastUpdate > throttle.interval then + throttle.lastUpdate = now + throttle.updatesThisInterval = 0 + end + + -- Check if we've exceeded max updates this interval + if throttle.updatesThisInterval >= throttle.maxUpdatesPerInterval then + if pfUI.uf.stats and pfUI.uf.stats.enabled then + pfUI.uf.stats.earlyReturns = pfUI.uf.stats.earlyReturns + 1 + end + return + end + + throttle.updatesThisInterval = throttle.updatesThisInterval + 1 + + -- Nampower not available or no data - trigger fallback update + this.update_base = true + if pfUI.uf.stats and pfUI.uf.stats.enabled then + pfUI.uf.stats.heartbeatUpdates = pfUI.uf.stats.heartbeatUpdates + 1 + end + else + -- Nampower working fine, no update needed + if pfUI.uf.stats and pfUI.uf.stats.enabled then + pfUI.uf.stats.earlyReturns = pfUI.uf.stats.earlyReturns + 1 + end + return + end + else + -- Too soon or unit doesn't exist + if pfUI.uf.stats and pfUI.uf.stats.enabled then + pfUI.uf.stats.earlyReturns = pfUI.uf.stats.earlyReturns + 1 + end + return + end + end + + -- process indicator update events + if this.update_indicators then + pfUI.uf:RefreshIndicators(this) + this.update_indicators = nil + end + + -- process all queued unit events + if this.update_full then + -- process full updates + pfUI.uf:RefreshUnit(this, "all") + + -- clear update caches + this.update_full = nil + this.update_base = nil + this.update_aura = nil + this.update_portrait = nil + this.update_pvp = nil + else + -- process individual events + if this.update_aura then + pfUI.uf:RefreshUnit(this, "aura") + this.update_aura = nil + this.update_base = true + end + + if this.update_portrait then + pfUI.uf:RefreshUnit(this, "portrait") + this.update_portrait = nil + this.update_base = true + end + + if this.update_pvp then + pfUI.uf:RefreshUnit(this, "pvp") + this.update_pvp = nil + end + + if this.update_base then + pfUI.uf:RefreshUnit(this, "base") + this.update_base = nil + end + end + + -- handle pseudo focus frames + if this.unitname and this == pfFocus then + local unitname = ( this.label and UnitName(this.label) ) or "" + + if pfFocusTarget then -- update focus target + pfFocusTarget.label = this.label and this.label .. "target" or nil + local focustargetname = pfFocusTarget.label and UnitName(pfFocusTarget.label) or nil + + if pfFocusTarget.lastUnit ~= focustargetname then + pfFocusTarget.lastUnit = focustargetname + pfFocusTarget.instantRefresh = true + pfUI.uf:RefreshUnit(pfFocusTarget, "all") + end + end + + -- break here on unset focus frames + if not this.unitname or this.unitname == "focus" then return end + + -- focus unit detection + if this.unitname ~= strlower(unitname) then + -- invalid focus frame + for unit, bool in pairs(pfValidUnits) do + local scan = UnitName(unit) or "" + if this.unitname == strlower(scan) then + this.label = unit + if this.portrait then this.portrait.model.lastUnit = nil end + this.instantRefresh = true + pfUI.uf:RefreshUnit(this, "all") + return + end + this.label = nil + this.instantRefresh = true + this.hp.bar:SetStatusBarColor(.2,.2,.2) + end + end + end + + -- handle pseudo focus target visibility + if this.unitname and this == pfFocusTarget then + if pfFocus and not pfFocus.label or pfFocus.label == "" then + this.label = nil + return + end + end + + if not this.label then return end + + -- update portrait on first visible frame + if this.portrait and this.portrait.model and this.portrait.model.update then + this.portrait.model.lastUnit = UnitName(this.portrait.model.update) + this.portrait.model:SetUnit(this.portrait.model.update) + this.portrait.model:SetCamera(0) + this.portrait.model.update = nil + end +end + +function pfUI.uf.OnEnter() + if not this.label then return end + + -- Nampower: Set native mouseover unit for macro/addon compatibility + if SetMouseoverUnit then + local unitstr = this.label .. this.id + -- For GUID-based frames (focus), use the GUID directly + if this.label and string.find(this.label, "^0x") then + SetMouseoverUnit(this.label) + elseif UnitExists(unitstr) then + SetMouseoverUnit(unitstr) + end + end + + if this.config.showtooltip == "0" then return end + GameTooltip_SetDefaultAnchor(GameTooltip, this) + GameTooltip:SetUnit(this.label .. this.id) + GameTooltip:Show() +end + +function pfUI.uf.OnLeave() + -- Nampower: Clear native mouseover unit + if SetMouseoverUnit then + SetMouseoverUnit() + end + + GameTooltip:FadeOut() +end + +function pfUI.uf.OnClick() + if not this.label and this.unitname then + TargetByName(this.unitname, true) + else + pfUI.uf:ClickAction(arg1) + end +end + +function pfUI.uf:RightClickAction(unit) + if unit == "player" then + ToggleDropDownMenu(1, nil, PlayerFrameDropDown, "cursor") + elseif unit == "target" then + ToggleDropDownMenu(1, nil, TargetFrameDropDown, "cursor") + elseif unit == "pet" then + ToggleDropDownMenu(1, nil, PetFrameDropDown, "cursor") + elseif unit == "party" or strfind(unit, "party%d") then + ToggleDropDownMenu(1, nil, getglobal("PartyMemberFrame" .. this.id .. "DropDown"), "cursor") + elseif unit == "raid" or strfind(unit, "raid%d") then + local name = this.lastUnit + local unitstr = this.label .. this.id + FriendsDropDown.displayMode = "MENU" + FriendsDropDown.initialize = function() UnitPopup_ShowMenu(_G[UIDROPDOWNMENU_OPEN_MENU], "PARTY", unitstr, name, id) end + ToggleDropDownMenu(1, nil, FriendsDropDown, "cursor") + end +end + +function pfUI.uf:EnableEvents() + local f = self + + f:RegisterEvent("PLAYER_ENTERING_WORLD") + f:RegisterEvent("PLAYER_LOGOUT") + f:RegisterEvent("UNIT_DISPLAYPOWER") + f:RegisterEvent("UNIT_HEALTH") + f:RegisterEvent("UNIT_MAXHEALTH") + f:RegisterEvent("UNIT_MANA") + f:RegisterEvent("UNIT_MAXMANA") + f:RegisterEvent("UNIT_RAGE") + f:RegisterEvent("UNIT_MAXRAGE") + f:RegisterEvent("UNIT_ENERGY") + f:RegisterEvent("UNIT_MAXENERGY") + f:RegisterEvent("UNIT_FOCUS") + f:RegisterEvent("UNIT_PORTRAIT_UPDATE") + f:RegisterEvent("UNIT_MODEL_CHANGED") + f:RegisterEvent("UNIT_FACTION") + f:RegisterEvent("UNIT_AURA") -- frame=buff, frame=debuff + f:RegisterEvent("PLAYER_AURAS_CHANGED") -- label=player && frame=buff + f:RegisterEvent("UNIT_INVENTORY_CHANGED") -- label=player && frame=buff + f:RegisterEvent("PARTY_MEMBERS_CHANGED") -- label=party, frame=leaderIcon + f:RegisterEvent("PARTY_LEADER_CHANGED") -- frame=leaderIcon + f:RegisterEvent("RAID_ROSTER_UPDATE") -- label=raidIcon + f:RegisterEvent("PLAYER_UPDATE_RESTING") -- label=restIcon + f:RegisterEvent("PLAYER_TARGET_CHANGED") -- label=target + f:RegisterEvent("PARTY_LOOT_METHOD_CHANGED") -- frame=lootIcon + f:RegisterEvent("RAID_TARGET_UPDATE") -- frame=raidIcon + f:RegisterEvent("UNIT_PET") + f:RegisterEvent("UNIT_HAPPINESS") + + f:RegisterForClicks('LeftButtonUp', 'RightButtonUp', + 'MiddleButtonUp', 'Button4Up', 'Button5Up') +end + +function pfUI.uf:EnableScripts() + local f = self + + -- handle secure unit button templates (> vanilla) + if pfUI.client > 11200 then + f.showmenu = pfUI.uf.RightClickAction + f:SetAttribute("*type1", "target") + f:SetAttribute("*type2", "showmenu") + else + f:SetScript("OnClick", pfUI.uf.OnClick) + end + + f:SetScript("OnShow", pfUI.uf.OnShow) + f:SetScript("OnEvent", pfUI.uf.OnEvent) + f:SetScript("OnUpdate", pfUI.uf.OnUpdate) + f:SetScript("OnEnter", pfUI.uf.OnEnter) + f:SetScript("OnLeave", pfUI.uf.OnLeave) + f:EnableClickCast() + + -- add frame to visibility refresh handler + visibilityscan.frames[f] = true +end + +function pfUI.uf:CreateUnitFrame(unit, id, config, tick) + local fname = (( unit == "Party" ) and "Group" or (unit or "")) .. (id or "") + local unit = strlower(unit or "") + local id = strlower(id or "") + + -- fake party0 units as self + if unit == "party" and id == "0" then + unit, id = "player", "" + end + + if unit == "partypet" and id == "0" then + unit, id = "pet", "" + end + + if unit == "pettarget" and id == "0" then + unit, id = "pettarget", "" + end + + if unit == "party0target" then + unit, id = "target", "" + end + + local f = CreateFrame("Button", "pf" .. fname, UIParent, UNITFRAME_SECURE_TEMPLATE) + + -- add unitframe functions + f.UpdateFrameSize = pfUI.uf.UpdateFrameSize + f.UpdateVisibility = pfUI.uf.UpdateVisibility + f.UpdateConfig = pfUI.uf.UpdateConfig + f.EnableScripts = pfUI.uf.EnableScripts + f.EnableEvents = pfUI.uf.EnableEvents + f.EnableClickCast = pfUI.uf.EnableClickCast + f.GetColor = pfUI.uf.GetColor + + -- cache values to the frame + f.label = strlower(unit) + f.fname = fname + f.id = id + f.config = config or pfUI_config.unitframes.fallback + f.tick = tick + + -- disable events for unknown unitstrings + if not pfValidUnits[strlower(unit) .. id] then + f.unitname = unit + f.label, f.id = "", "" + f.RegisterEvent = function() return end + end + + CreateBackdropShadow(f) + + f.hp = CreateFrame("Frame",nil, f) + f.hp.bar = CreateStatusBar(nil, f.hp) + + f.power = CreateFrame("Frame",nil, f) + f.power.bar = CreateStatusBar(nil, f.power) + + f.glow = CreateFrame("Frame", nil, f) + f.combat = CreateFrame("Frame", nil, f.hp.bar) + f.combat.tex = f.combat:CreateTexture(nil, "OVERLAY") + f.combat.tex:SetAllPoints() + + f.texts = CreateFrame("Frame", nil, f) + f.texts:SetFrameLevel(16) + f.texts:SetAllPoints() + + f.hpLeftText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + f.hpRightText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + f.hpCenterText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + f.powerLeftText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + f.powerRightText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + f.powerCenterText = f.texts:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + + f.incHeal = CreateFrame("Frame", nil, f.hp) + -- Texture lives on hp.bar for correct draw order: + -- hp.bar.bg (BACKGROUND) < incHeal (BORDER) < hp.bar.bar (NORMAL) + f.incHeal.texture = f.hp.bar:CreateTexture(nil, "BORDER") + f.incHeal.texture:SetAllPoints(f.incHeal) + -- Override Show/Hide since texture lives on hp.bar, not incHeal + f.incHeal._Show = f.incHeal.Show + f.incHeal._Hide = f.incHeal.Hide + f.incHeal.Show = function(self) self:_Show() self.texture:Show() end + f.incHeal.Hide = function(self) self:_Hide() self.texture:Hide() end + f.incHeal.texture:Hide() + + f.ressIcon = CreateFrame("Frame", nil, f.hp.bar) + f.ressIcon.texture = f.ressIcon:CreateTexture(nil,"BACKGROUND") + + f.leaderIcon = CreateFrame("Frame", nil, f.hp.bar) + f.leaderIcon.texture = f.leaderIcon:CreateTexture(nil,"BACKGROUND") + + f.lootIcon = CreateFrame("Frame",nil, f.hp.bar) + f.lootIcon.texture = f.lootIcon:CreateTexture(nil,"BACKGROUND") + + f.pvpIcon = CreateFrame("Frame", nil, f.hp.bar) + f.pvpIcon.texture = f.pvpIcon:CreateTexture(nil,"BACKGROUND") + + f.raidIcon = CreateFrame("Frame", nil, f.hp.bar) + f.raidIcon.texture = f.raidIcon:CreateTexture(nil,"ARTWORK") + + f.restIcon = CreateFrame("Frame", nil, f.hp.bar) + f.restIcon.texture = f.restIcon:CreateTexture(nil, "BACKGROUND") + + f.happinessIcon = CreateFrame("Frame", nil, f.hp.bar) + f.happinessIcon.texture = f.happinessIcon:CreateTexture(nil, "BACKGROUND") + + f.portrait = CreateFrame("Frame", "pfPortrait" .. f.label .. f.id, f) + f.portrait.tex = f.portrait:CreateTexture("pfPortraitTexture" .. f.label .. f.id, "OVERLAY") + f.portrait.model = CreateFrame("PlayerModel", "pfPortraitModel" .. f.label .. f.id, f.portrait) + f.portrait.model.next = CreateFrame("PlayerModel", nil, nil) + f.feedbackText = f:CreateFontString("pfHitIndicator" .. f.label .. f.id, "OVERLAY", "NumberFontNormalHuge") + + if f.label == "raid" and mod(f.id, 5) == 1 then + local group = math.ceil(f.id/5) + f.group = f.group or f.hp.bar:CreateFontString("Status", "OVERLAY", "GameFontNormalSmall") + f.group:SetFont(pfUI.font_unit, 8, "OUTLINE") + f.group:SetTextColor(1,1,1,.8) + f.group:SetHeight(16) + f.group:SetText("Group " .. group) + f.group:Hide() + end + + f:Hide() + f:UpdateConfig() + f:UpdateFrameSize() + f:EnableScripts() + f:EnableEvents() + + if f.config.visible == "1" then + pfUI.uf:RefreshUnit(f, "all") + f:EnableScripts() + f:EnableEvents() + f:UpdateFrameSize() + else + f:UnregisterAllEvents() + f:Hide() + end + + -- register frame for clique + _G.ClickCastFrames = ClickCastFrames or {} + ClickCastFrames[f] = true + + table.insert(pfUI.uf.frames, f) + return f +end + +function pfUI.uf:RefreshUnitState(unit) + local alpha = unit.alpha_visible + local unlock = pfUI.unlock and pfUI.unlock:IsShown() or nil + + if not UnitIsConnected(unit.label .. unit.id) and not unlock then + -- offline + alpha = unit.alpha_offline + unit.hp.bar:SetMinMaxValues(0, 100, true) + unit.power.bar:SetMinMaxValues(0, 100, true) + unit.hp.bar:SetValue(0) + unit.power.bar:SetValue(0) + elseif unit.config.faderange == "1" and not pfUI.api.UnitInRange(unit.label .. unit.id, 4) and not unlock then + alpha = unit.alpha_outrange + end + + -- skip if alpha is already correct + if floor(unit:GetAlpha()*10+.5) == floor(alpha*10+.5) then return end + + -- set unitframe alpha + unit:SetAlpha(alpha) + + -- refresh portrait alpha + if unit.config.portrait == "bar" then + unit.portrait:SetAlpha(pfUI_config.unitframes.portraitalpha) + end + + -- refresh debuff indicator alpha + local disptype = unit.config.debuff_indicator + local indicator = unit.hp.bar.debuffindicators + if indicator then + indicator:SetAlpha(0) + if ( disptype == "4" or disptype == "3" ) then + indicator:SetAlpha(1) + elseif disptype == "2" then + indicator:SetAlpha(.4) + elseif disptype == "1" then + indicator:SetAlpha(.2) + end + end +end + +function pfUI.uf:RefreshIndicators(unit) + if not unit.label or not unit.id then return end + local unitstr = unit.label .. unit.id + + if unit.leaderIcon then -- Leader Icon + if unit.config.leadericon == "1" and UnitIsPartyLeader(unitstr) and ( GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 ) then + unit.leaderIcon:Show() + else + unit.leaderIcon:Hide() + end + end + + if unit.lootIcon then -- Loot Icon + if unit.config.looticon == "0" then + unit.lootIcon:Hide() + else + -- no third return value here.. but leaving this as a hint + local method, group, raid = GetLootMethod() + local name = group and UnitName(group == 0 and "player" or "party"..group) or raid and UnitName("raid"..raid) or nil + + if name and name == UnitName(unitstr) then + unit.lootIcon:Show() + else + unit.lootIcon:Hide() + end + end + end + + if unit.pvpIcon then -- PvP Icon + if unit.config.showPVP == "1" and UnitIsPVP(unitstr) then + unit.pvpIcon:Show() + else + unit.pvpIcon:Hide() + end + end + + if unit.restIcon and unit:GetName() == "pfPlayer" then -- Rest Icon + if C.unitframes.player.showRest == "1" and UnitIsUnit(unitstr, "player") and IsResting() then + unit.restIcon:Show() + else + unit.restIcon:Hide() + end + end + + if unit.happinessIcon and unit:GetName() == "pfPet" then -- Happiness Icon + local _, pclass = UnitClass("player") + if unit.config.happinessicon == "0" or pclass ~= "HUNTER" then + unit.happinessIcon:Hide() + else + if UnitIsVisible("pet") then + local happiness = GetPetHappiness() + if happiness == 1 then + unit.happinessIcon.texture:SetTexture(pfUI.media["img:sad"..unit.config.happinessicon]) + unit.happinessIcon.texture:SetVertexColor(1, 0, 0, 1) + elseif happiness == 2 then + unit.happinessIcon.texture:SetTexture(pfUI.media["img:neutral"..unit.config.happinessicon]) + unit.happinessIcon.texture:SetVertexColor(1, 1, 0, 1) + else + unit.happinessIcon.texture:SetTexture(pfUI.media["img:happy"..unit.config.happinessicon]) + unit.happinessIcon.texture:SetVertexColor(0, 1, 0, 1) + end + unit.happinessIcon:Show() + else + unit.happinessIcon:Hide() + end + end + end + + if unit.raidIcon then -- Raid Icon + local raidIcon = UnitName(unitstr) and GetRaidTargetIndex(unitstr) + if unit.config.raidicon == "1" and raidIcon then + SetRaidTargetIconTexture(unit.raidIcon.texture, raidIcon) + unit.raidIcon:Show() + else + unit.raidIcon:Hide() + end + end +end + +local pfDebuffColors = { + ["Magic"] = { 0.1, 0.7, 0.8, 1 }, + ["Poison"] = { 0.2, 0.7, 0.3, 1 }, + ["Curse"] = { 0.6, 0.2, 0.6, 1 }, + ["Disease"] = { 0.9, 0.7, 0.2, 1 } +} + +function pfUI.uf:RefreshUnit(unit, component) + -- break early on misconfigured UF's + if not unit.label then return end + if not unit.hp then return end + if not unit.power then return end + if not unit.id then unit.id = "" end + local component = component or "" + + -- don't update scanner activity + if unit.label == "target" or unit.label == "targettarget" or unit.label == "targettargettarget" then + if pfScanActive == true then return end + end + + -- hide unused and invalid frames + unit:UpdateVisibility() + + -- return on invisible unit frames + if not unit:IsShown() and not unit.visible then return end + + -- create required fields + local unitstr = unit.label..unit.id + local rawborder, default_border = GetBorderSize("unitframes") + + -- save current values + unit.namecache = UnitName(unitstr) + + -- buffs + if unit.buffs and ( component == "all" or component == "aura" ) then + local texture, stacks + + for i=1, unit.config.bufflimit do + if not unit.buffs[i] then break end + + if unit.label == "player" then + stacks = GetPlayerBuffApplications(GetPlayerBuff(PLAYER_BUFF_START_ID+i,"HELPFUL")) + texture = GetPlayerBuffTexture(GetPlayerBuff(PLAYER_BUFF_START_ID+i,"HELPFUL")) + else + texture, stacks = pfUI.uf:DetectBuff(unitstr, i) + end + + unit.buffs[i].texture:SetTexture(texture) + + if texture then + unit.buffs[i]:Show() + + if stacks > 1 then + unit.buffs[i].stacks:SetText(stacks) + else + unit.buffs[i].stacks:SetText("") + end + else + unit.buffs[i]:Hide() + end + end + end + + -- debuffs + if unit.debuffs and ( component == "all" or component == "aura" ) then + local texture, stacks, dtype + local perrow = unit.config.debuffperrow + local bperrow = unit.config.buffperrow + local selfdebuff = unit.config.selfdebuff + + local invert_h, invert_v, af + if unit.config.debuffs == "TOPLEFT" then + invert_h = 1 + invert_v = 1 + af = "BOTTOMLEFT" + elseif unit.config.debuffs == "BOTTOMLEFT" then + invert_h = -1 + invert_v = 1 + af = "TOPLEFT" + elseif unit.config.debuffs == "TOPRIGHT" then + invert_h = 1 + invert_v = -1 + af = "BOTTOMRIGHT" + elseif unit.config.debuffs == "BOTTOMRIGHT" then + invert_h = -1 + invert_v = -1 + af = "TOPRIGHT" + end + + local buffrow, reposition = 0, ( component == "all" and true or nil ) + if unit.config.buffs == unit.config.debuffs then + if unit.buffs[0*bperrow+1] and unit.buffs[0*bperrow+1]:IsShown() then buffrow = buffrow + 1 end + if unit.buffs[1*bperrow+1] and unit.buffs[1*bperrow+1]:IsShown() then buffrow = buffrow + 1 end + if unit.buffs[2*bperrow+1] and unit.buffs[2*bperrow+1]:IsShown() then buffrow = buffrow + 1 end + if unit.buffs[3*bperrow+1] and unit.buffs[3*bperrow+1]:IsShown() then buffrow = buffrow + 1 end + end + + if buffrow ~= unit.lastbuffrow then + unit.lastbuffrow = buffrow + reposition = true + end + + for i=1, unit.config.debufflimit do + if not unit.debuffs[i] then break end + + local row = floor((i-1) / unit.config.debuffperrow) + + if reposition then + local anchor = unit.config.portraitheight ~= "-1" and unit.hp or unit + if anchor == unit.hp and (unit.config.debuffs == "BOTTOMLEFT" or unit.config.debuffs == "BOTTOMRIGHT") then + anchor = unit.power + end + local multiply = C.appearance.border.force_blizz == "1" and 1 or 2 + unit.debuffs[i]:SetPoint(af, anchor, unit.config.debuffs, + invert_v * (i-1-row*perrow)*(multiply*default_border + unit.config.debuffsize + 1), + invert_h * ((row+buffrow)*(multiply*default_border + unit.config.debuffsize + 1) + (multiply*default_border + 1))) + end + + if unit.label == "player" then + texture = GetPlayerBuffTexture(GetPlayerBuff(PLAYER_BUFF_START_ID+i, "HARMFUL")) + stacks = GetPlayerBuffApplications(GetPlayerBuff(PLAYER_BUFF_START_ID+i, "HARMFUL")) + dtype = GetPlayerBuffDispelType(GetPlayerBuff(PLAYER_BUFF_START_ID+i, "HARMFUL")) + elseif selfdebuff == "1" then + _, _, texture, stacks, dtype = libdebuff:UnitOwnDebuff(unitstr, i) + else + texture, stacks, dtype = UnitDebuff(unitstr, i) + end + + unit.debuffs[i].texture:SetTexture(texture) + + local r,g,b = DebuffTypeColor.none.r,DebuffTypeColor.none.g,DebuffTypeColor.none.b + if dtype and DebuffTypeColor[dtype] then + r,g,b = DebuffTypeColor[dtype].r,DebuffTypeColor[dtype].g,DebuffTypeColor[dtype].b + end + unit.debuffs[i].backdrop:SetBackdropBorderColor(r,g,b,1) + + if texture then + unit.debuffs[i]:Show() + + if unit:GetName() == "pfPlayer" then + local timeleft = GetPlayerBuffTimeLeft(GetPlayerBuff(PLAYER_BUFF_START_ID+unit.debuffs[i].id, "HARMFUL"),"HARMFUL") + CooldownFrame_SetTimer(unit.debuffs[i].cd, GetTime(), timeleft, 1) + elseif libdebuff and selfdebuff == "1" then + local name, rank, texture, stacks, dtype, duration, timeleft, caster = libdebuff:UnitOwnDebuff(unitstr, i) + if duration and timeleft then + CooldownFrame_SetTimer(unit.debuffs[i].cd, GetTime() + timeleft - duration, duration, 1) + end + elseif libdebuff then + local name, rank, texture, stacks, dtype, duration, timeleft, caster = libdebuff:UnitDebuff(unitstr, i) + if duration and timeleft then + CooldownFrame_SetTimer(unit.debuffs[i].cd, GetTime() + timeleft - duration, duration, 1) + end + end + + if stacks > 1 then + unit.debuffs[i].stacks:SetText(stacks) + else + unit.debuffs[i].stacks:SetText("") + end + else + unit.debuffs[i]:Hide() + end + end + end + + -- indicators + if component == "all" or component == "aura" then + if not unit.dispellable and unit.config.debuff_indicator ~= "0" then + unit.dispellable = pfUI.uf:SetupDebuffFilter((unit.config.debuff_ind_class == "0" and true or nil)) + elseif not unit.dispellable then + unit.dispellable = {} + end + + if table.getn(unit.dispellable) > 0 then + unit.hp.bar.debuffindicators = unit.hp.bar.debuffindicators or CreateFrame("Frame", nil, unit.hp.bar) + + -- 0 = OFF, 1 = Legacy, 2 = Glow, 3 = Square, 4 = Icons + local disptype = unit.config.debuff_indicator + local indicator = unit.hp.bar.debuffindicators + local indipos = unit.config.debuff_ind_pos + local count = 0 + local size + + if disptype == "4" or disptype == "3" then + size = unit.hp.bar:GetHeight() * tonumber(unit.config.debuff_ind_size) + if size ~= indicator.size or disptype ~= indicator.disp or indipos ~= indicator.ipos then + indicator:ClearAllPoints() + indicator:SetPoint(indipos, 0, 0) + indicator:SetHeight(size) + indicator:SetWidth(size) + indicator.size = size + indicator.disp = disptype + indicator.ipos = indipos + end + elseif disptype == "2" or disptype == "1" then + size = "FULL" + if size ~= indicator.size or disptype ~= indicator.disp or indipos ~= indicator.ipos then + indicator:ClearAllPoints() + indicator:SetAllPoints(unit.hp.bar) + indicator.size = size + indicator.disp = disptype + indicator.ipos = indipos + end + end + + for _, debuff in pairs(unit.dispellable) do + indicator[debuff] = indicator[debuff] or CreateFrame("Frame", nil, indicator) + indicator[debuff]:SetParent(indicator) + indicator[debuff].tex = indicator[debuff].tex or indicator[debuff]:CreateTexture(nil) + indicator[debuff].tex:SetAllPoints(indicator[debuff]) + + if indicator.size ~= indicator[debuff].size or disptype ~= indicator[debuff].disp then + if disptype == "4" then + indicator[debuff].tex:SetTexture(pfUI.media["img:"..debuff]) + indicator[debuff].tex:SetVertexColor(unpack(pfDebuffColors[debuff])) + indicator[debuff].tex:Show() + indicator[debuff]:ClearAllPoints() + indicator[debuff]:SetHeight(size) + indicator[debuff]:SetWidth(size) + indicator[debuff]:SetBackdrop(nil) + elseif disptype == "3" then + indicator[debuff].tex:SetTexture(unpack(pfDebuffColors[debuff])) + indicator[debuff].tex:SetVertexColor(1,1,1,1) + indicator[debuff].tex:Show() + indicator[debuff]:ClearAllPoints() + indicator[debuff]:SetHeight(size) + indicator[debuff]:SetWidth(size) + indicator[debuff]:SetBackdrop(nil) + elseif disptype == "2" then + indicator[debuff].tex:Hide() + indicator[debuff]:SetAllPoints(unit.hp.bar) + indicator[debuff]:SetBackdrop(glow) + indicator[debuff]:SetBackdropBorderColor(unpack(pfDebuffColors[debuff])) + elseif disptype == "1" then + indicator[debuff].tex:SetTexture(unpack(pfDebuffColors[debuff])) + indicator[debuff].tex:SetVertexColor(1,1,1,1) + indicator[debuff].tex:Show() + indicator[debuff]:SetAllPoints(unit.hp.bar) + indicator[debuff]:SetBackdrop(nil) + end + + indicator[debuff].size = indicator.size + indicator[debuff].disp = indicator.disp + end + + indicator[debuff].visible = nil + + for i=1,16 do + local _, _, dtype = UnitDebuff(unitstr, i) + if dtype == debuff then + indicator[debuff].visible = true + end + end + + if indicator[debuff].visible then + indicator[debuff]:Show() + indicator:Show() + indicator:SetAlpha(0) + if disptype == "4" or disptype == "3" then + indicator:SetAlpha(1) + elseif disptype == "2" then + indicator:SetAlpha(.4) + elseif disptype == "1" then + indicator:SetAlpha(.2) + end + + if disptype == "4" or disptype == "3" then + indicator[debuff]:SetPoint("LEFT", indicator, "LEFT", count*(size+1), 0) + count = count + 1 + end + else + indicator[debuff]:Hide() + end + end + + if disptype == "4" or disptype == "3" then + indicator:SetWidth(count*(size+1)) + end + elseif unit.hp.bar.debuffindicators then + unit.hp.bar.debuffindicators:Hide() + end + + if not unit.indicators and unit.config.buff_indicator == "1" then + unit.indicators = pfUI.uf:SetupBuffIndicators(unit.config) + elseif not unit.indicators then + unit.indicators = {} + end + + if not unit.indicator_custom and unit.config.buff_indicator == "1" then + unit.indicator_custom = {} + for k, v in pairs({strsplit("#", unit.config.custom_indicator)}) do + unit.indicator_custom[k] = string.lower(v) + end + elseif not unit.indicator_custom then + unit.indicator_custom = {} + end + + local pos = 1 + if table.getn(unit.indicators) > 0 then + for i=1,32 do + local texture, count = UnitBuff(unitstr, i) + local timeleft, buffName, _ + if pfUI.client > 11200 then + buffName, _, texture, _, _, timeleft = _G.UnitBuff(unitstr, i) + end + + if texture then + -- match filter + for _, filter in pairs(unit.indicators) do + if filter == string.lower(texture) then + if string.lower(texture) == "interface\\icons\\spell_nature_rejuvenation" then + -- Also verify spell name to avoid false matches from spells sharing this icon + if not buffName then + scanner = scanner or libtipscan:GetScanner("unitframes") + scanner:SetUnitBuff(unitstr, i) + buffName = scanner:Line(1) or "" + end + if string.lower(buffName) ~= "rejuvenation" then break end + local start, duration, prediction = libpredict:GetHotDuration(unitstr, "Reju") + pfUI.uf:AddIcon(unit, pos, texture, timeleft or prediction, count, tonumber(start), tonumber(duration)) + pos = pos + 1 + break + elseif string.lower(texture) == "interface\\icons\\spell_holy_renew" then + local start, duration, prediction = libpredict:GetHotDuration(unitstr, "Renew") + pfUI.uf:AddIcon(unit, pos, texture, timeleft or prediction, count, tonumber(start), tonumber(duration)) + pos = pos + 1 + break + elseif string.lower(texture) == "interface\\icons\\spell_nature_resistnature" then + -- Also verify spell name to avoid false matches from spells sharing this icon + if not buffName then + scanner = scanner or libtipscan:GetScanner("unitframes") + scanner:SetUnitBuff(unitstr, i) + buffName = scanner:Line(1) or "" + end + if string.lower(buffName) ~= "regrowth" then break end + local start, duration, prediction = libpredict:GetHotDuration(unitstr, "Regr") + pfUI.uf:AddIcon(unit, pos, texture, timeleft or prediction, count, tonumber(start), tonumber(duration)) + pos = pos + 1 + break + else + pfUI.uf:AddIcon(unit, pos, texture, timeleft, count) + pos = pos + 1 + break + end + end + end + end + end + end + + if table.getn(unit.indicator_custom) > 0 then + scanner = scanner or libtipscan:GetScanner("unitframes") + + for i=1,32 do -- scan for custom buffs + local texture, count = UnitBuff(unitstr, i) + if texture then + local timeleft, name, _ + if pfUI.client > 11200 then + name, _, texture, _, _, timeleft = _G.UnitBuff(unitstr, i) + else + scanner:SetUnitBuff(unitstr, i) + name = scanner:Line(1) or "" + end + + -- match filter + for _, filter in pairs(unit.indicator_custom) do + if filter == string.lower(name) then + pfUI.uf:AddIcon(unit, pos, texture, timeleft, count) + pos = pos + 1 + break + end + end + end + end + + for i=1,32 do -- scan for custom debuffs + local texture, count = UnitDebuff(unitstr, i) + if texture then + local timeleft, name, _ + if libdebuff then + -- Use UnitOwnDebuff if "show only own debuffs" is enabled + if unit.config.selfdebuff == "1" then + name, _, texture, _, _, _, timeleft = libdebuff:UnitOwnDebuff(unitstr, i) + else + name, _, texture, _, _, _, timeleft = libdebuff:UnitDebuff(unitstr, i) + end + else + scanner:SetUnitDebuff(unitstr, i) + name = scanner:Line(1) or "" + end + + -- match filter + if name then + for _, filter in pairs(unit.indicator_custom) do + if filter == string.lower(name) then + pfUI.uf:AddIcon(unit, pos, texture, timeleft, count) + pos = pos + 1 + break + end + end + end + end + end + end + + -- hide unused icon slots + for pos=pos, 6 do pfUI.uf:HideIcon(unit, pos) end + end + + -- portrait + if unit.portrait and ( component == "all" or component == "portrait" ) then + if C.unitframes.always2dportrait == "1" then + unit.portrait.tex:Show() + unit.portrait.model:Hide() + SetPortraitTexture(unit.portrait.tex, unitstr) + else + if not UnitIsVisible(unitstr) or not UnitIsConnected(unitstr) then + if unit.config.portrait == "bar" then + unit.portrait.tex:Hide() + unit.portrait.model:Hide() + elseif C.unitframes.portraittexture == "1" then + unit.portrait.tex:Show() + unit.portrait.model:Hide() + SetPortraitTexture(unit.portrait.tex, unitstr) + else + unit.portrait.tex:Hide() + unit.portrait.model:Show() + unit.portrait.model:SetModelScale(4.25) + unit.portrait.model:SetPosition(0, 0, -1) + unit.portrait.model:SetModel("Interface\\Buttons\\talktomequestionmark.mdx") + end + else + if unit.config.portrait == "bar" then + unit.portrait:SetAlpha(C.unitframes.portraitalpha) + else + unit.portrait:SetAlpha(1) + end + unit.portrait.tex:Hide() + unit.portrait.model:Show() + + if component == "portrait" then + -- regular portrait update after event + unit.portrait.model.update = unitstr + else + -- detect portrait change without events + unit.portrait.model.next:SetUnit(unitstr) + if unit.portrait.model.lastUnit ~= UnitName(unitstr) or unit.portrait.model:GetModel() ~= unit.portrait.model.next:GetModel() then + unit.portrait.model.update = unitstr + end + end + end + end + end + + -- base frame + if component == "all" or component == "base" then + -- Unit HP/MP with Nampower Integration + local hp, hpmax, power, powermax, powerType = pfUI.api.GetUnitStats(unitstr, true) + + -- Store original values for color calculations (before invert_healthbar modifies hp) + local hp_orig, hpmax_orig = hp, hpmax + + if unit.config.invert_healthbar == "1" then + hp = hpmax - hp + end + + unit.hp.bar:SetMinMaxValues(0, hpmax, true) + unit.hp.bar:SetValue(hp) + + unit.power.bar:SetMinMaxValues(0, powermax, true) + unit.power.bar:SetValue(power) + + -- Hide power bar text for NPCs without real power (power == 0) + local isNPC = not UnitIsPlayer(unitstr) and not UnitPlayerControlled(unitstr) + local npcNoPower = isNPC and (not power or power == 0) + + -- set healthbar color + local custom_active = nil + local customfullhp = unit.config.defcolor == "0" and unit.config.customfullhp or C.unitframes.customfullhp + local customcolor = unit.config.defcolor == "0" and unit.config.customcolor or C.unitframes.customcolor + local customfade = unit.config.defcolor == "0" and unit.config.customfade or C.unitframes.customfade + local custom = unit.config.defcolor == "0" and unit.config.custom or C.unitframes.custom + + local r, g, b, a = .2, .2, .2, 1 + -- O(1) optimization: use cached hp/hpmax instead of UnitHealth()/UnitHealthMax() API calls + if customfullhp == "1" and hp_orig == hpmax_orig then + r, g, b, a = GetStringColor(customcolor) + custom_active = true + elseif custom == "0" then + if UnitIsPlayer(unitstr) then + local _, class = UnitClass(unitstr) + local color = RAID_CLASS_COLORS[class] + if color then r, g, b = color.r, color.g, color.b end + elseif unit.label == "pet" then + local happiness = GetPetHappiness() + if happiness == 1 then + r, g, b = 1, 0, 0 + elseif happiness == 2 then + r, g, b = 1, 1, 0 + else + r, g, b = 0, 1, 0 + end + else + local color = UnitReactionColor[UnitReaction(unitstr, "player")] + if color then r, g, b = color.r, color.g, color.b end + end + elseif custom == "1" then + r, g, b, a = GetStringColor(customcolor) + custom_active = true + elseif custom == "2" then + -- O(1) optimization: use cached hp/hpmax instead of UnitHealth()/UnitHealthMax() API calls + if hpmax_orig > 0 then + r, g, b = GetColorGradient(hp_orig / hpmax_orig) + else + r, g, b = 0, 0, 0 + end + end + + if C.unitframes.pastel == "1" and not custom_active then + r, g, b = (r + .5) * .5, (g + .5) * .5, (b + .5) * .5 + end + + if customfade == "1" then + -- fade custom color into default color + -- O(1) optimization: use cached hp/hpmax instead of UnitHealth()/UnitHealthMax() API calls + local perc = hpmax_orig > 0 and (hp_orig / hpmax_orig) or 0 + local cr, cg, cb, ca = GetStringColor(customcolor) + + r = (cr*perc) + (r*(1-perc)) + g = (cg*perc) + (g*(1-perc)) + b = (cb*perc) + (b*(1-perc)) + end + + unit.hp.bar:SetStatusBarColor(r, g, b, a) + + -- set powerbar color + local mana = unit.config.defcolor == "0" and unit.config.manacolor or C.unitframes.manacolor + local rage = unit.config.defcolor == "0" and unit.config.ragecolor or C.unitframes.ragecolor + local energy = unit.config.defcolor == "0" and unit.config.energycolor or C.unitframes.energycolor + local focus = unit.config.defcolor == "0" and unit.config.focuscolor or C.unitframes.focuscolor + + local r, g, b, a = .5, .5, .5, 1 + local utype = UnitPowerType(unitstr) + if utype == 0 then + r, g, b, a = GetStringColor(mana) + elseif utype == 1 then + r, g, b, a = GetStringColor(rage) + elseif utype == 2 then + r, g, b, a = GetStringColor(focus) + elseif utype == 3 then + r, g, b, a = GetStringColor(energy) + end + + unit.power.bar:SetStatusBarColor(r, g, b, a) + + if UnitName(unitstr) then + unit.hpLeftText:SetText(pfUI.uf:GetStatusValue(unit, "hpleft")) + unit.hpCenterText:SetText(pfUI.uf:GetStatusValue(unit, "hpcenter")) + unit.hpRightText:SetText(pfUI.uf:GetStatusValue(unit, "hpright")) + + -- Hide power text for NPCs without a real power system + local cfgLeft = unit.config.txtpowerleft + local cfgCenter = unit.config.txtpowercenter + local cfgRight = unit.config.txtpowerright + if npcNoPower and cfgLeft and strfind(cfgLeft, "power") then + unit.powerLeftText:SetText("") + else + unit.powerLeftText:SetText(pfUI.uf:GetStatusValue(unit, "powerleft")) + end + if npcNoPower and cfgCenter and strfind(cfgCenter, "power") then + unit.powerCenterText:SetText("") + else + unit.powerCenterText:SetText(pfUI.uf:GetStatusValue(unit, "powercenter")) + end + if npcNoPower and cfgRight and strfind(cfgRight, "power") then + unit.powerRightText:SetText("") + else + unit.powerRightText:SetText(pfUI.uf:GetStatusValue(unit, "powerright")) + end + + if UnitIsTapped(unitstr) and not UnitIsTappedByPlayer(unitstr) then + unit.hp.bar:SetStatusBarColor(.5,.5,.5,.5) + end + end + + pfUI.uf:RefreshUnitState(unit) + end +end + +local buttons = { + [1] = "LeftButton", + [2] = "RightButton", + [3] = "MiddleButton", + [4] = "Button4", + [5] = "Button5", +} + +local modifiers = { + [""] = "", + ["alt"] = "_alt", + ["ctrl"] = "_ctrl", + ["shift"] = "_shift", +} + +function pfUI.uf:EnableClickCast() + if self.config.clickcast ~= "1" then return end + for bid, button in pairs(buttons) do + for modifier, mconf in pairs(modifiers) do + local bconf = bid == 1 and "" or bid + if pfUI_config.unitframes["clickcast"..bconf..mconf] ~= "" then + -- prepare click casting + if pfUI.client > 11200 then + -- set attributes for tbc+ + local prefix = modifier == "" and "" or modifier .. "-" + + -- check for "/" in the beginning of the string, to detect macros + local ccval = pfUI_config.unitframes["clickcast"..bconf..mconf] + local ccval_lower = string.lower(ccval) + if string.find(ccval, "^%/(.+)") then + self:SetAttribute(prefix.."type"..bid, "macro") + self:SetAttribute(prefix.."macrotext"..bid, ccval) + self:SetAttribute(prefix.."spell"..bid, nil) + elseif string.find(ccval_lower, "^target") then + self:SetAttribute(prefix.."type"..bid, "target") + self:SetAttribute(prefix.."macrotext"..bid, nil) + self:SetAttribute(prefix.."spell"..bid, nil) + elseif string.find(ccval_lower, "^menu") then + self:SetAttribute(prefix.."type"..bid, "showmenu") + self:SetAttribute(prefix.."macrotext"..bid, nil) + self:SetAttribute(prefix.."spell"..bid, nil) + else + self:SetAttribute(prefix.."type"..bid, "spell") + self:SetAttribute(prefix.."spell"..bid, pfUI_config.unitframes["clickcast"..bconf..mconf]) + self:SetAttribute(prefix.."macro"..bid, nil) + end + else + -- fill clickaction table for vanillla + self.clickactions = self.clickactions or {} + self.clickactions[modifier..button] = pfUI_config.unitframes["clickcast"..bconf..mconf] + end + end + end + end +end + +function pfUI.uf:ClickAction(button) + local label = this.label or "" + local id = this.id or "" + local unitstr = label .. id + local showmenu = button == "RightButton" and true or nil + if SpellIsTargeting() and button == "RightButton" then + SpellStopTargeting() + return + end + + if SpellIsTargeting() and button == "LeftButton" then + SpellTargetUnit(unitstr) + elseif CursorHasItem() then + DropItemOnUnit(unitstr) + end + + -- run click casting if enabled + local modstring = "" + modstring = IsAltKeyDown() and modstring.."alt" or modstring + modstring = IsControlKeyDown() and modstring.."ctrl" or modstring + modstring = IsShiftKeyDown() and modstring.."shift" or modstring + modstring = modstring..button + if this.clickactions and this.clickactions[modstring] then + local action_lower = string.lower(this.clickactions[modstring]) + if string.find(action_lower, "^menu") then + -- show menu + showmenu = true + elseif string.find(action_lower, "^target") then + -- target unit + showmenu = nil + else + -- run click cast action + local is_macro = string.find(this.clickactions[modstring], "^%/(.+)") + + local tswitch = UnitIsUnit(unitstr, "target") + TargetUnit(unitstr) + + if is_macro then + RunMacroText(this.clickactions[modstring]) + else + CastSpellByName(this.clickactions[modstring]) + end + + if not tswitch then TargetLastTarget() end + + return + end + end + + -- dropdown menus + if showmenu then + pfUI.uf:RightClickAction(label) + return + end + + -- drop food on petframe + if label == "pet" and CursorHasItem() then + local _, playerClass = UnitClass("player") + if playerClass == "HUNTER" then + DropItemOnUnit("pet") + return + end + end + + -- default click + TargetUnit(unitstr) +end + +function pfUI.uf:AddIcon(frame, pos, icon, timeleft, stacks, start, duration) + local showtime = frame.config.indicator_time == "1" and true or nil + local showstacks = frame.config.indicator_stacks == "1" and true or nil + local position = frame.config.indicator_pos or "TOPLEFT" + local iconsize = tonumber(frame.config.indicator_size) + local spacing = tonumber(frame.config.indicator_spacing) + + if not frame.hp then return end + local frame = frame.hp.bar + if pos > 6 or pos > ceil(frame:GetWidth() / iconsize) then return end + + frame.icon = frame.icon or CreateFrame("Frame", nil, frame) + + if not frame.icon[pos] then + frame.icon[pos] = CreateFrame("Frame", nil, frame.icon) + frame.icon[pos]:SetParent(frame) + frame.icon[pos].tex = frame.icon[pos]:CreateTexture("OVERLAY") + frame.icon[pos].tex:SetAllPoints(frame.icon[pos]) + frame.icon[pos].tex:SetTexCoord(.08, .92, .08, .92) + frame.icon[pos].stacks = frame.icon[pos]:CreateFontString(nil, "OVERLAY") + frame.icon[pos].stacks:SetPoint("BOTTOMRIGHT", 0, 0) + frame.icon[pos].stacks:SetJustifyH("RIGHT") + frame.icon[pos].stacks:SetJustifyV("BOTTOM") + + -- Check if parent frame has cooldown animation enabled + local parent_cooldown_anim = frame.config and tonumber(frame.config.cooldown_anim) or 1 + if parent_cooldown_anim == 1 then + frame.icon[pos].cd = CreateFrame(COOLDOWN_FRAME_TYPE, nil, frame.icon[pos]) + else + frame.icon[pos].cd = CreateFrame("Frame", nil, frame.icon[pos]) + frame.icon[pos].cd.AdvanceTime = DoNothing + frame.icon[pos].cd.SetSequence = DoNothing + frame.icon[pos].cd.SetSequenceTime = DoNothing + end + + frame.icon[pos].cd.pfCooldownStyleAnimation = 0 + frame.icon[pos].cd.pfCooldownType = "ALL" + frame.icon[pos].cd:SetFrameLevel(48) + end + + -- update icon configuration + if frame.icon[pos].iconsize ~= iconsize or frame.icon[pos].spacing ~= spacing then + frame.icon[pos]:SetWidth(iconsize) + frame.icon[pos]:SetHeight(iconsize) + frame.icon[pos]:SetPoint("TOPLEFT", frame.icon, "TOPLEFT", (pos-1)*(iconsize + spacing), 0) + frame.icon[pos].stacks:SetFont(pfUI.font_unit, math.max(iconsize/3, 10), "OUTLINE") + frame.icon[pos].iconsize = iconsize + frame.icon[pos].spacing = spacing + end + + -- update icon + if frame.icon[pos].icon ~= icon then + frame.icon[pos].tex:SetTexture(icon) + frame.icon[pos].icon = icon + end + + -- show remaining time if config is set + if showtime and start and duration and timeleft < 100 and iconsize > 9 then + CooldownFrame_SetTimer(frame.icon[pos].cd, start, duration, 1) + elseif showtime and timeleft and timeleft < 100 and iconsize > 9 then + CooldownFrame_SetTimer(frame.icon[pos].cd, GetTime(), timeleft, 1) + else + CooldownFrame_SetTimer(frame.icon[pos].cd, GetTime(), 0, 1) + end + + -- show stacks if config is set + if showstacks and stacks and stacks > 1 and iconsize > 9 then + frame.icon[pos].stacks:SetText(stacks) + else + frame.icon[pos].stacks:SetText("") + end + + -- update parent icon size + if frame.icon.iconsize ~= iconsize then + frame.icon:SetHeight(iconsize) + frame.icon.iconsize = iconsize + end + + -- update parent position + if frame.icon.position ~= position then + frame.icon:ClearAllPoints() + frame.icon:SetPoint(position, frame, position, 0, 0) + frame.icon.position = position + end + + frame.icon[pos]:Show() + frame.icon:SetWidth((pos-1)*(iconsize+spacing)+iconsize) +end + +function pfUI.uf:HideIcon(frame, pos) + if not frame or not frame.hp or not frame.hp.bar then return end + + local frame = frame.hp.bar + if frame.icon and frame.icon[pos] then + frame.icon[pos]:Hide() + end +end + +function pfUI.uf:SetupDebuffFilter(allclasses) + local _, myclass = UnitClass("player") + local debuffs = {} + + if myclass == "PALADIN" or myclass == "PRIEST" or myclass == "WARLOCK" or allclasses then + table.insert(debuffs, "Magic") + end + + if myclass == "DRUID" or myclass == "PALADIN" or myclass == "SHAMAN" or allclasses then + table.insert(debuffs, "Poison") + end + + if myclass == "PRIEST" or myclass == "PALADIN" or myclass == "SHAMAN" or allclasses then + table.insert(debuffs, "Disease") + end + + if myclass == "DRUID" or myclass == "MAGE" or allclasses then + table.insert(debuffs, "Curse") + end + + return debuffs +end + +function pfUI.uf:SetupBuffIndicators(config) + local _, myclass = UnitClass("player") + local indicators = {} + + if config.show_buffs == "1" then -- buffs + if myclass == "DRUID" then + -- Mark of the Wild + table.insert(indicators, "interface\\icons\\spell_nature_regeneration") + -- Gift of the Wild + table.insert(indicators, "interface\\icons\\spell_nature_giftofthewild") + -- Thorns + table.insert(indicators, "interface\\icons\\spell_nature_thorns") + end + + if myclass == "PRIEST" then + -- Prayer Of Fortitude" + table.insert(indicators, "interface\\icons\\spell_holy_wordfortitude") + table.insert(indicators, "interface\\icons\\spell_holy_prayeroffortitude") + -- Prayer of Spirit + table.insert(indicators, "interface\\icons\\spell_holy_divinespirit") + table.insert(indicators, "interface\\icons\\spell_holy_prayerofspirit") + -- Shadow Protection + table.insert(indicators, "interface\\icons\\spell_shadow_antishadow") + table.insert(indicators, "interface\\icons\\spell_holy_prayerofshadowprotection") + -- Fear Ward + table.insert(indicators, "interface\\icons\\spell_holy_excorcism") + end + + if myclass == "PALADIN" then + -- Blessing of Salvation + table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofsalvation") + table.insert(indicators, "interface\\icons\\spell_holy_sealofsalvation") + -- Blessing of Wisdom + table.insert(indicators, "interface\\icons\\spell_holy_sealofwisdom") + table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofwisdom") + -- Blessing of Sanctuary + table.insert(indicators, "interface\\icons\\spell_nature_lightningshield") + table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofsanctuary") + -- Blessing of Kings + table.insert(indicators, "interface\\icons\\spell_magic_magearmor") + table.insert(indicators, "interface\\icons\\spell_magic_greaterblessingofkings") + -- Blessing of Might + table.insert(indicators, "interface\\icons\\spell_holy_fistofjustice") + table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingofkings") + -- Blessing of Light + table.insert(indicators, "interface\\icons\\spell_holy_prayerofhealing02") + table.insert(indicators, "interface\\icons\\spell_holy_greaterblessingoflight") + -- Blessing of Sacrifice + table.insert(indicators, "interface\\icons\\spell_holy_sealofsacrifice") + -- Blessing of Freedom + table.insert(indicators, "interface\\icons\\spell_holy_sealofvalor") + -- Blessing of Protection + table.insert(indicators, "interface\\icons\\spell_holy_sealofprotection") + end + + if myclass == "WARLOCK" then + -- Fire Shield + table.insert(indicators, "interface\\icons\\spell_fire_firearmor") + -- Blood Pact + table.insert(indicators, "interface\\icons\\spell_shadow_bloodboil") + -- Soulstone + table.insert(indicators, "interface\\icons\\spell_shadow_soulgem") + -- Unending Breath + table.insert(indicators, "interface\\icons\\spell_shadow_demonbreath") + -- Detect Greater Invisibility or Detect Invisibility + table.insert(indicators, "interface\\icons\\spell_shadow_detectinvisibility") + -- Detect Lesser Invisibility + table.insert(indicators, "interface\\icons\\spell_shadow_detectlesserinvisibility") + -- Paranoia + table.insert(indicators, "interface\\icons\\Spell_Shadow_AuraOfDarkness") + end + + if myclass == "WARRIOR" then + -- Battle Shout + table.insert(indicators, "interface\\icons\\ability_warrior_battleshout") + -- Commanding Shout (TBC) + table.insert(indicators, "interface\\icons\\ability_warrior_rallyingcry") + end + + if myclass == "MAGE" then + -- Arcane Intellect + table.insert(indicators, "interface\\icons\\spell_holy_magicalsentry") + table.insert(indicators, "interface\\icons\\spell_holy_arcaneintellect") + -- Dampen Magic + table.insert(indicators, "interface\\icons\\spell_nature_abolishmagic") + -- Amplify Magic + table.insert(indicators, "interface\\icons\\spell_holy_flashheal") + end + + if myclass == "HUNTER" then + -- Aspect of the Wild + table.insert(indicators, "interface\\icons\\spell_nature_protectionformnature") + + -- Aspect of the Pack + table.insert(indicators, "interface\\icons\\ability_mount_whitetiger") + + -- Misdirection (TBC) + table.insert(indicators, "interface\\icons\\ability_hunter_misdirection") + end + + if myclass == "SHAMAN" then + -- Earth Shield (TBC) + table.insert(indicators, "interface\\icons\\spell_nature_skinofearth") + end + end + + if config.show_procs == "1" then -- procs + if myclass == "SHAMAN" or config.all_procs == "1" then + -- Ancestral Fortitude + table.insert(indicators, "interface\\icons\\spell_nature_undyingstrength") + -- Healing Way + table.insert(indicators, "interface\\icons\\spell_nature_healingway") + -- Totemic Power (known issue: one conflicts with Blessed Sunfruit buff) + table.insert(indicators, "interface\\icons\\spell_holy_spiritualguidence") + table.insert(indicators, "interface\\icons\\spell_holy_devotion") + table.insert(indicators, "interface\\icons\\spell_holy_holynova") + table.insert(indicators, "interface\\icons\\spell_magic_magearmor") + end + + if myclass == "PRIEST" or config.all_procs == "1" then + -- Inspiration + table.insert(indicators, "interface\\icons\\inv_shield_06") + end + end + + if config.show_hots == "1" then -- hots + if myclass == "PRIEST" or config.all_hots == "1" then + -- Renew + table.insert(indicators, "interface\\icons\\spell_holy_renew") + -- Power Word: Shield + table.insert(indicators, "interface\\icons\\spell_holy_powerwordshield") + -- Prayer of Mending (TBC) + table.insert(indicators, "interface\\icons\\spell_holy_prayerofmendingtga") + end + + if myclass == "DRUID" or config.all_hots == "1" then + -- Regrowth + table.insert(indicators, "interface\\icons\\spell_nature_resistnature") + -- Rejuvenation + table.insert(indicators, "interface\\icons\\spell_nature_rejuvenation") + -- Lifebloom + table.insert(indicators, "interface\\icons\\inv_misc_herb_felblossom") + end + end + + if config.show_totems == "1" and myclass == "SHAMAN" then -- totems + -- Strength of Earth Totem + table.insert(indicators, "interface\\icons\\spell_nature_earthbindtotem") + -- Stoneskin Totem + table.insert(indicators, "interface\\icons\\spell_nature_stoneskintotem") + -- Mana Spring Totem + table.insert(indicators, "interface\\icons\\spell_nature_manaregentotem") + -- Mana Tide Totem + table.insert(indicators, "interface\\icons\\spell_frost_summonwaterelemental") + -- Healing Spring Totem + table.insert(indicators, "interface\\icons\\inv_spear_04") + -- Tranquil Air Totem + table.insert(indicators, "interface\\icons\\spell_nature_brilliance") + -- Grace of Air Totem + table.insert(indicators, "interface\\icons\\spell_nature_invisibilitytotem") + -- Grounding Totem + table.insert(indicators, "interface\\icons\\spell_nature_groundingtotem") + -- Nature Resistance Totem + table.insert(indicators, "interface\\icons\\spell_nature_natureresistancetotem") + -- Fire Resistance Totem + table.insert(indicators, "interface\\icons\\spell_fireresistancetotem_01") + -- Frost Resistance Totem + table.insert(indicators, "interface\\icons\\spell_frostresistancetotem_01") + end + + return indicators +end + +local function abbrevname(t) + return string.sub(t,1,1)..". " +end + +function pfUI.uf:GetNameString(unitstr) + local name = UnitName(unitstr) + local abbrev = pfUI_config.unitframes.abbrevname == "1" or nil + local size = 20 + + -- first try to only abbreviate the first word + if abbrev and name and strlen(name) > size then + name = string.gsub(name, "^(%S+) ", abbrevname) + end + + -- abbreviate all if it still doesn't fit + if abbrev and name and strlen(name) > size then + name = string.gsub(name, "(%S+) ", abbrevname) + end + + return name +end + +function pfUI.uf:GetLevelString(unitstr) + local level = UnitLevel(unitstr) + if level == -1 then level = "??" end + + local elite = UnitClassification(unitstr) + if elite == "worldboss" then + level = level .. "B" + elseif elite == "rareelite" then + level = level .. "R+" + elseif elite == "elite" then + level = level .. "+" + elseif elite == "rare" then + level = level .. "R" + end + + return level +end + +function pfUI.uf:GetStatusValue(unit, pos) + if not pos or not unit then return end + local config = unit.config["txt"..pos] + local unitstr = unit.label .. unit.id + local frame = unit[pos .. "Text"] + + -- as a fallback, draw the name + if pos == "center" and not config then + config = "unit" + end + + -- Get stats with Nampower Integration + local hp, hpmax, mp, mpmax, powerType = pfUI.api.GetUnitStats(unitstr, true) + local rhp, rhpmax = hp, hpmax + + -- Use libhealth for mob health estimation (overrides Nampower/Standard) + if pfUI.libhealth and pfUI.libhealth.enabled then + rhp, rhpmax = pfUI.libhealth:GetUnitHealth(unitstr) + elseif unit.label == "target" and (MobHealth3 or MobHealthFrame) and MobHealth_GetTargetCurHP() then + rhp, rhpmax = MobHealth_GetTargetCurHP(), MobHealth_GetTargetMaxHP() + end + + if config == "unit" then + local name = unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) + local level = unit:GetColor("level") .. pfUI.uf:GetLevelString(unitstr) + + return level .. " " .. name + elseif config == "unitrev" then + local name = unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) + local level = unit:GetColor("level") .. pfUI.uf:GetLevelString(unitstr) + + return name .. " " .. level + elseif config == "name" then + return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) + elseif config == "nameshort" then + return unit:GetColor("unit") .. strsub(UnitName(unitstr), 0, 3) + elseif config == "level" then + return unit:GetColor("level") .. pfUI.uf:GetLevelString(unitstr) + elseif config == "class" then + if UnitIsPlayer(unitstr) then + return unit:GetColor("class") .. (UnitClass(unitstr) or UNKNOWN) + else + return "" + end + + -- health + elseif config == "health" then + return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp) + elseif config == "healthmax" then + return unit:GetColor("health") .. pfUI.api.Abbreviate(rhpmax) + elseif config == "healthperc" then + return unit:GetColor("health") .. ceil(hp / hpmax * 100) + elseif config == "healthmiss" then + local health = ceil(rhp - rhpmax) + if UnitIsDead(unitstr) then + return unit:GetColor("health") .. DEAD + elseif health == 0 then + return unit:GetColor("health") .. "0" + else + return unit:GetColor("health") .. pfUI.api.Abbreviate(health) + end + elseif config == "healthdyn" then + if hp ~= hpmax then + return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp) .. " - " .. ceil(hp / hpmax * 100) .. "%" + else + return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp) + end + elseif config == "namehealth" then + local health = ceil(rhp - rhpmax) + if UnitIsDead(unitstr) then + return unit:GetColor("health") .. DEAD + elseif health == 0 then + return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) + else + return unit:GetColor("health") .. pfUI.api.Abbreviate(health) + end + elseif config == "namehealthbreak" then + local health = ceil(rhp - rhpmax) + if UnitIsDead(unitstr) then + return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) .. "\n" .. unit:GetColor("health") .. DEAD + elseif health == 0 then + return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) + else + return unit:GetColor("unit") .. pfUI.uf:GetNameString(unitstr) .. "\n" .. unit:GetColor("health") .. pfUI.api.Abbreviate(-health) + end + elseif config == "shortnamehealth" then + local health = ceil(rhp - rhpmax) + if UnitIsDead(unitstr) then + return unit:GetColor("health") .. DEAD + elseif health == 0 then + return unit:GetColor("unit") .. strsub(UnitName(unitstr), 0, 3) + else + return unit:GetColor("health") .. pfUI.api.Abbreviate(health) + end + elseif config == "healthminmax" then + return unit:GetColor("health") .. pfUI.api.Abbreviate(rhp) .. "/" .. pfUI.api.Abbreviate(rhpmax) + + -- mana/power/focus + elseif config == "power" then + return unit:GetColor("power") .. pfUI.api.Abbreviate(mp) + elseif config == "powermax" then + return unit:GetColor("power") .. pfUI.api.Abbreviate(mpmax) + elseif config == "powerperc" then + local perc = UnitManaMax(unitstr) > 0 and ceil(mp / mpmax * 100) or 0 + return unit:GetColor("power") .. perc + elseif config == "powermiss" then + local power = ceil(mp - mpmax) + if power == 0 then + return unit:GetColor("power") .. "0" + else + return unit:GetColor("power") .. pfUI.api.Abbreviate(power) + end + elseif config == "powerdyn" then + -- show percentage when only mana is less than 100% + if mp ~= mpmax and UnitPowerType(unitstr) == 0 then + return unit:GetColor("power") .. pfUI.api.Abbreviate(mp) .. " - " .. ceil(mp / mpmax * 100) .. "%" + else + return unit:GetColor("power") .. pfUI.api.Abbreviate(mp) + end + elseif config == "powerminmax" then + return unit:GetColor("power") .. pfUI.api.Abbreviate(mp) .. "/" .. pfUI.api.Abbreviate(mpmax) + else + return "" + end +end + +function pfUI.uf.GetColor(self, preset) + local config = self.config + + local unitstr = self.label .. self.id + local r, g, b = 1, 1, 1 + + if preset == "unit" and config["classcolor"] == "1" then + if UnitIsPlayer(unitstr) then + local _, class = UnitClass(unitstr) + if RAID_CLASS_COLORS[class] then + r, g, b = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b + end + elseif self.label == "pet" then + local happiness = GetPetHappiness() + if happiness == 1 then + r, g, b = 1, 0, 0 + elseif happiness == 2 then + r, g, b = 1, 1, 0 + else + r, g, b = 0, 1, 0 + end + else + local color = UnitReactionColor[UnitReaction(unitstr, "player")] + if color then r, g, b = color.r, color.g, color.b end + end + + elseif preset == "class" and config["classcolor"] == "1" then + local _, class = UnitClass(unitstr) + if RAID_CLASS_COLORS[class] then + r, g, b = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b + end + + elseif preset == "reaction" and config["classcolor"] == "1" then + r = UnitReactionColor[UnitReaction(unitstr, "player")].r + g = UnitReactionColor[UnitReaction(unitstr, "player")].g + b = UnitReactionColor[UnitReaction(unitstr, "player")].b + + elseif preset == "health" and config["healthcolor"] == "1" then + -- O(1) Nampower lookup for health gradient color + local hp, hpmax + if GetUnitField then + local guid = GetUnitGUID(unitstr) + if guid then + hp = GetUnitField(guid, "health") + hpmax = GetUnitField(guid, "maxHealth") + end + end + -- Fallback to standard API + if not hp or not hpmax then + hp, hpmax = UnitHealth(unitstr), UnitHealthMax(unitstr) + end + if hpmax and hpmax > 0 then + r, g, b = GetColorGradient(hp / hpmax) + else + r, g, b = 0, 0, 0 + end + + elseif preset == "power" and config["powercolor"] == "1" then + r = ManaBarColor[UnitPowerType(unitstr)].r + g = ManaBarColor[UnitPowerType(unitstr)].g + b = ManaBarColor[UnitPowerType(unitstr)].b + elseif preset == "level" and config["levelcolor"] == "1" then + r = GetDifficultyColor(UnitLevel(unitstr)).r + g = GetDifficultyColor(UnitLevel(unitstr)).g + b = GetDifficultyColor(UnitLevel(unitstr)).b + end + + if C.unitframes.pastel == "1" then + r, g, b = (r + .75) * .5, (g + .75) * .5, (b + .75) * .5 + end + + return rgbhex(r,g,b) +end + +-- ============================================================================ +-- Slash Commands for Stats Frame +-- ============================================================================ +_G.SLASH_PFUISTATS1 = "/pfuistats" +_G.SLASH_PFUISTATS2 = "/ufstats" +_G.SlashCmdList["PFUISTATS"] = function(msg) + msg = string.lower(msg or "") + + if not pfUI.uf.stats then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR:|r Stats not initialized!") + return + end + + -- Initialize startTime on first use + if pfUI.uf.stats.startTime == 0 then + pfUI.uf.stats.startTime = GetTime() + end + + if msg == "reset" then + pfUI.uf.stats.eventUpdates = 0 + pfUI.uf.stats.heartbeatUpdates = 0 + pfUI.uf.stats.earlyReturns = 0 + pfUI.uf.stats.nampowerUsed = 0 + pfUI.uf.stats.fallbackUsed = 0 + pfUI.uf.stats.throttledSkips = 0 + pfUI.uf.stats.startTime = GetTime() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00pfUI Stats:|r Reset!") + + elseif msg == "toggle" then + pfUI.uf.stats.enabled = not pfUI.uf.stats.enabled + local status = pfUI.uf.stats.enabled and "|cff00ff00ON|r" or "|cffff0000OFF|r" + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00pfUI Stats:|r Tracking: " .. status) + + elseif msg == "show" then + if pfUI.uf.statsFrame then + pfUI.uf.statsFrame:Show() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00pfUI Stats:|r Frame shown") + end + + elseif msg == "hide" then + if pfUI.uf.statsFrame then + pfUI.uf.statsFrame:Hide() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00pfUI Stats:|r Frame hidden") + end + + else + -- Toggle frame (default action) + if pfUI.uf.statsFrame then + if pfUI.uf.statsFrame:IsShown() then + pfUI.uf.statsFrame:Hide() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00pfUI Stats:|r Frame hidden") + else + pfUI.uf.statsFrame:Show() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00pfUI Stats:|r Frame shown") + end + end + end +end \ No newline at end of file