Files
pfUI/api/unitframes.lua
Brues f497c71523 Filter unit events to their units, rework energytick, drop macrotweak
Eight commits off classicapi_next.

ClassicAPI's RegisterUnitEvent registers for an event but only delivers it
when arg1 is one of the given units, so a handler for one unit stops waking
for every other one in the world. The 26 registrations whose unit set is
fixed at registration time now name it. The rule throughout is register the
superset and keep the handler's own check -- the filter narrows what arrives,
it does not decide what to act on. Guards that look unreachable stay put: the
filter applies only when arg1 is a string, so an event that fires with a
number or no argument is delivered as if plainly registered.

Frames whose unit changes at runtime own their subscriptions instead of
sorting events out per event. unitframes points them at the unitstr
UpdateVisibility already computes -- replacing a string concat, and on a miss
a second concat plus a UnitGUID call, for every frame on every unit event in
the world -- and a frame that is not in use drops its unit events entirely.
nameplates registers per plate against the plate's own token, which is also
the only workable shape: slots have no cap, so any nameplate1..N list would
have been a guess that fails in exactly the crowded scenes where plates
matter. marktracking names mark1 through mark8. A registration keeps its
kind, so none of these can be plain-registered first.

Both teardown paths PLAYER_LOGOUT guards -- the crash 132 -- now cover the
per-frame subscriptions: plates tear down rather than dispatching through
logout, and a unit frame takes itself off the visibility scan so it cannot
re-register what it just dropped.

marktracking also drops its once-a-second full rebuild, which ran for the
whole session whether or not a marker existed anywhere. The ticker is created
and cancelled with group membership. It is deliberately not keyed on a mark
being visible -- a marker on an out-of-range unit shows no row, and that is
the case the poll exists to catch.

nameplates gates the per-plate update against the floor across all four
throttle categories before classifying it, instead of running a GetAlpha, a
castbar IsShown, a cast lookup and up to two libthrottle:Get resolutions on
plates throttled to 10fps that were going to return anyway. Nothing that
would have updated can be turned away by a floor. The four throttles resolve
in CacheConfig, where config changes already land.

energytick sweeps the clock the server actually runs. There is one regen
timer for every power, re-armed every 2s by Player::RegenerateAll and never
touched by casting; the five-second rule changes what a tick pays, not when
it lands. The sweep is a free-running phase lock on that clock, so
Illumination refunds, potions and a Mana Spring totem on its own phase no
longer snap the spark mid-cycle, and an 80ms band keeps a correct tick from
hitching it at the wrap. The FSR window shades rather than predicting a share
of spirit the client cannot compute -- the Casting Regen item ladder is equip
auras absent from the buff list. The energy period is summed from
SPELL_AURA_MOD_ENERGY_REGEN_TIME across the spellbook and buffs, so Blade
Rush is found without GetTalentInfo(2, 16), an ordinal that does not fail
when the tree changes but reads another talent's rank.

macrotweak is gone -- ClassicAPI 1.15 covers it -- with its config entry, its
GUI block, its translations in all eight locales, and actionbar's
ButtonMacroScan, the #showtooltip scanner that fed it.
2026-09-11 00:10:51 -05:00

2778 lines
102 KiB
Lua

-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
pfUI.uf = CreateFrame("Frame", nil, UIParent)
pfUI.uf.frames = {}
-- Reusable buffer for C_UnitAuras.GetAuraSlots slot ids (filled by
-- ScanAuraSlots, api.lua). Each aura scan in RefreshUnit fills it once, then
-- reads every aura by slot id: one array walk per scan instead of one per index.
-- Scans run back to back and consume the buffer before the next refill.
local auraSlots = {}
-- Separate buffer for the tooltip handlers. They run from OnEnter, which can
-- fire while RefreshUnit is showing/hiding icons under the cursor, so they must
-- not share the refresh buffer.
local tooltipSlots = {}
-- ============================================================================
-- 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
pfUI.api.RegisterSlashCommand("PFTEST", { "/pftest", "/pfuftest" }, function()
pfUI.uf.showall = not pfUI.uf.showall
if pfUI.uf.raid and pfUI.uf.raid.LayoutPets then pfUI.uf.raid:LayoutPets() end
end, true)
-- Buff indicators are identified by a spell id, resolved once into the aura's
-- localized name plus its icon. A match needs both to agree.
--
-- Icon alone is ambiguous: Spell.dbc reuses icons across unrelated spells, so
-- an icon-only filter lights the indicator for the wrong buff (Blessing of
-- Sanctuary shares its icon with Lightning Shield and Shadowguard, Blessing of
-- Kings with Mage Armor and Commanding Shout, Totemic Power with the Blessed
-- Sunfruit food buff). Name alone is ambiguous too -- creature and item auras
-- reuse player spell names ("Renew", "Rejuvenation", "Fire Resistance").
--
-- Every rank of a spell carries the same name and icon, so one id per buff
-- covers the whole rank ladder. Ids missing from this client resolve to nil and
-- drop out of the list. 'predict' names the libpredict key of a HoT.
--
-- Both fields are stored raw, and RefreshUnit compares them raw. The aura's
-- name and icon and these come out of the same DBC records byte for byte --
-- Spell.dbc's localized name, SpellIcon.dbc's path -- so case folding either
-- side would only burn a string per aura per scan. Feed this ids, never
-- hand-written names or icon paths, or that equality quietly stops holding.
local indicator_cache = {}
local function AddIndicator(indicators, spellId, predict)
local record = indicator_cache[spellId]
if record == nil then
local name = C_Spell.GetSpellName(spellId)
local icon = name and C_Spell.GetSpellTexture(spellId)
-- cache misses as false, so an absent spell is only looked up once
record = icon and { name = name, icon = icon, predict = predict } or false
indicator_cache[spellId] = record
end
if record then table.insert(indicators, record) end
end
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 function BuffOnEnter()
local parent = this:GetParent()
if not parent.label then return end
local unit = parent.label .. parent.id
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitAura(unit, this.id, "HELPFUL")
if IsShiftKeyDown() then
local aura = C_UnitAuras.GetAuraDataByIndex(unit, this.id, "HELPFUL")
local playerlist = aura and GetUnbuffedRoster(aura.name) or ""
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
local aura = C_UnitAuras.GetAuraDataByIndex("player", this.id, "HELPFUL")
if aura and aura.spellId then C_Spell.CancelSpellByID(aura.spellId) end
end
end
local function DebuffOnEnter()
if not this:GetParent().label then return end
local unitstr = this:GetParent().label .. this:GetParent().id
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
if this:GetParent().label ~= "player" then
local parent = this:GetParent()
-- selfdebuff filters the displayed list to player-cast harmful auras, but
-- SetUnitAura's index has to be into the unfiltered HARMFUL list. Look up
-- the displayed aura via the PLAYER filter, then find its position in the
-- unfiltered list by name + sourceGUID.
--
-- The unfiltered list is NOT capped at 16: once a unit's 16 debuff slots
-- are full the server parks further debuffs in buff slots, and C_UnitAuras
-- reports those as harmful too. So enumerate however many the unit has.
if parent.config and parent.config.selfdebuff == "1" then
local ownAura = C_UnitAuras.GetAuraDataByIndex(unitstr, this.id, "HARMFUL|PLAYER")
if ownAura then
local n = ScanAuraSlots(unitstr, "HARMFUL", tooltipSlots)
for i = 1, n do
local check = C_UnitAuras.GetAuraDataBySlot(unitstr, tooltipSlots[i])
if check and check.name == ownAura.name and check.sourceGUID == ownAura.sourceGUID then
GameTooltip:SetUnitAura(unitstr, i, "HARMFUL")
return
end
end
end
end
end
GameTooltip:SetUnitAura(unitstr, this.id, "HARMFUL")
end
local function DebuffOnLeave()
GameTooltip:Hide()
end
local function DebuffOnClick()
if this:GetParent().label == "player" then
local aura = C_UnitAuras.GetAuraDataByIndex("player", this.id, "HARMFUL")
if aura and aura.spellId then C_Spell.CancelSpellByID(aura.spellId) end
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 - health + power for a unit token.
-- Returns: hp, maxHp, power, maxPower, powerType
-- ============================================================================
function pfUI.api.GetUnitStats(unitstr)
local powerType = UnitPowerType(unitstr) or 0
return UnitHealth(unitstr) or 0,
UnitHealthMax(unitstr) or 1,
UnitPower(unitstr, powerType) or 0,
UnitPowerMax(unitstr, powerType) or 1,
powerType
end
local aggrodata = { }
local aggroScan = nil -- static { u, t, tt } triples, built once so the hot scan allocates nothing
function pfUI.api.UnitHasAggro(unit)
local now = GetTime()
local data = aggrodata[unit]
-- Cache positive results 1s (so aggro clears fast) AND negative results 0.3s
-- (so we don't rescan the whole unit table on every call while nothing has aggro).
if data and now < data.check + (data.state > 0 and 1 or 0.3) then
return data.state
end
if not data then data = { }; aggrodata[unit] = data end
data.check = now
data.state = 0
if UnitExists(unit) and UnitIsFriend(unit, "player") then
-- pfValidUnits never changes after load, so precompute the "<u>target" /
-- "<u>targettarget" token strings once instead of concatenating per call.
if not aggroScan then
aggroScan = {}
for u in pairs(pfValidUnits) do
local t = u .. "target"
table.insert(aggroScan, { u = u, t = t, tt = t .. "target" })
end
end
for i = 1, table.getn(aggroScan) do
local s = aggroScan[i]
if UnitExists(s.t) and UnitIsUnit(s.t, unit) and UnitCanAttack(s.u, unit) then
data.state = data.state + 1
end
if UnitExists(s.tt) and UnitIsUnit(s.tt, unit) and UnitCanAttack(s.t, unit) then
data.state = data.state + 1
end
end
end
return data.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
function pfUI.uf:UpdateVisibility()
local self = self or this
-- cache result of strsub to avoid repeating calls
if not self.cache_raid then
local name = self:GetName()
if strsub(name,0,9) == "pfRaidPet" then
-- pet grid slot (pfRaidPet1..40); used to mirror party pets below
self.cache_raid = 0
self.cache_raidpet = tonumber(strsub(name,10)) or 0
elseif strsub(name,0,6) == "pfRaid" then
self.cache_raid = tonumber(strsub(name,7,8)) or 0
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 IsInGroup() and C.unitframes.selfinraid == "1" and id == 1 then
self.id = ""
self.label = "player"
-- use raidframes for groups
elseif not IsInRaid() and IsInGroup() 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
-- show raidpet frames as party pets when a party is shown as a raid grid
if self.cache_raidpet then
if not IsInRaid() and IsInGroup() and C.unitframes.raidforgroup == "1" then
local id = self.cache_raidpet
if id == 1 then
-- grid slot 1 mirrors the player, so its pet is the player's pet
self.id = ""
self.label = "pet"
elseif id <= 5 then
self.id = id - 1
self.label = "partypet"
end
-- reset to regular raidpet unitstrings after leaving party mode
elseif self.label == "pet" or self.label == "partypet" then
self.id = self.cache_raidpet
self.label = "raidpet"
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 = ("%s%s"):format(self.label or "", self.id or "")
self.unitstr = unitstr ~= "" and unitstr or nil
self:SetAttribute("unit", self.unitstr)
local visibility = ("[target=%s,exists] show; hide"):format(unitstr)
-- Group frames are redundant when the group is already shown as a raid grid:
-- either an actual raid, or a party promoted to the raid grid via
-- raidforgroup. hide_in_raid gates whether we hide them in that case.
local hide_group = C["unitframes"]["group"]["hide_in_raid"] == "1"
and (IsInRaid() or (C.unitframes.raidforgroup == "1" and IsInGroup()))
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 hide_group and self.cache_raid == 0 and not self.cache_raidpet and self.label and strsub(self.label,0,5) == "party" then
-- hide group while shown as a raid grid and option is set (raid player
-- frames carry label "party" and raid pet frames carry label "partypet"
-- under raidforgroup, so exclude them by cache_raid / cache_raidpet)
visibility = "hide"
self.visible = nil
elseif ( self.fname == "Group0" or self.fname == "PartyPet0" or self.fname == "Party0Target" )
and (not IsInGroup() or hide_group) then
-- hide self in group if solo or shown as a raid grid
visibility = "hide"
self.visible = nil
end
-- This is the single place a frame's unit is ever assigned, so it is also the
-- place its subscriptions follow it to: the engine then delivers only this
-- unit's events, and OnEvent compares against the cached string instead of
-- rebuilding label..id -- and calling UnitGUID -- for every unit event fired
-- by anything, anywhere. A frame that is not in use drops them entirely; the
-- roster events that would bring it back are registered plainly, and
-- visibilityscan re-runs this every 0.2s regardless, so it recovers on its own.
self:RegisterUnitEvents(visibility ~= "hide" and self.unitstr or nil)
-- vanilla visibility
if self.unitname 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 == "raidpet" then
if not UnitIsVisible(unitstr) or not UnitExists("raid" .. 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:SetSize(real_height, real_height)
portrait = real_height + spacing + 2*default_border
else
-- use custom portrait size
self.portrait:SetSize(ptwidth, ptheight)
portrait = ptwidth + spacing + 2*default_border
end
end
self:SetSize(width + portrait, 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:SetSize(tonumber(f.config.squaresize), 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:SetSize(f.config.width, 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:SetSize((f.config.pwidth ~= "-1" and f.config.pwidth or f.config.width), f.config.pheight)
if tonumber(f.config.pheight) < 0 then f.power:Hide() end
pfUI.api.CreateBackdrop(f.power, default_border)
f.power.bar:SetFrameLevel(f.power.backdrop:GetFrameLevel() + 1)
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
-- Druid secondary mana bar: texture/color/size/position below the power bar,
-- using its own C.unitframes.druidmana* config. Values are read in
-- UpdateDruidMana; here we only lay it out.
if f.druidmana then
local DC = C.unitframes
local dmTexture = DC.druidmanatexture or "Interface\\AddOns\\pfUI\\img\\bar"
f.druidmana:SetStatusBarTexture(pfUI.media[dmTexture] or dmTexture)
f.druidmana:SetFrameLevel(f:GetFrameLevel() + 5)
local manacolor = f.config.defcolor == "0" and f.config.manacolor or C.unitframes.manacolor
f.druidmana:SetStatusBarColor(GetStringColor(manacolor))
local dmHeight = tonumber(DC.druidmanaheight) or 10
local dmWidth = DC.druidmanawidth or "-1"
local dmOffX = tonumber(DC.druidmanaoffx) or 0
local dmOffY = tonumber(DC.druidmanaoffy) or 0
local dmSpace = tonumber(DC.druidmanaspace) or -3
local dmSpacing = -2 * default_border - dmSpace
f.druidmana:SetHeight(dmHeight)
f.druidmana:ClearAllPoints()
local w = dmWidth ~= "-1" and tonumber(dmWidth) or nil
if w then
f.druidmana:SetWidth(w)
f.druidmana:SetPoint("TOP", f.power, "BOTTOM", dmOffX, dmSpacing + dmOffY)
else
f.druidmana:SetPoint("TOPLEFT", f.power, "BOTTOMLEFT", dmOffX, dmSpacing + dmOffY)
f.druidmana:SetPoint("TOPRIGHT", f.power, "BOTTOMRIGHT", dmOffX, dmSpacing + dmOffY)
end
if not f.druidmana._hasbd then
CreateBackdrop(f.druidmana, default_border)
CreateBackdropShadow(f.druidmana)
f.druidmana._hasbd = true
end
local tr, tg, tb = ManaBarColor[Enum.PowerType.Mana].r, ManaBarColor[Enum.PowerType.Mana].g, ManaBarColor[Enum.PowerType.Mana].b
if C.unitframes.pastel == "1" then
tr, tg, tb = (tr + .75) * .5, (tg + .75) * .5, (tb + .75) * .5
end
f.druidmana.text:SetFont(fontname, fontsize, fontstyle)
f.druidmana.text:SetTextColor(tr, tg, tb, 1)
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
f.group:SetShown(f.config.raidgrouplabel == "1")
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.combatfeedback = true
else
f.feedbackText:Hide()
f.combatfeedback = nil
end
-- RegisterUnitEvents owns UNIT_COMBAT; clearing the cached unit makes the
-- next UpdateVisibility re-run it against the new combatfeedback state.
f.eventunit = nil
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:SetSize(f.config.width, f.config.height)
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:SetSize(32, 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:SetSize(10, 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:SetSize(10, 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:SetSize(f.config.pvpiconsize, 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:SetSize(f.config.raidiconsize, 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:SetSize(16, 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:SetSize(tonumber(C.unitframes.pet.happinesssize), 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]:SetSize(f.config.buffsize, 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("Model", 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
f.buffs[i].cd.pfCooldownText:SetShown(cooldown_text == 1)
end
f.buffs[i].id = i
f.buffs[i]:Hide()
CreateBackdrop(f.buffs[i], default_border)
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]:SetSize(f.config.debuffsize, 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("Model", 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
f.debuffs[i].cd.pfCooldownText:SetShown(cooldown_text == 1)
end
f.debuffs[i].id = i
f.debuffs[i]:Hide()
CreateBackdrop(f.debuffs[i], default_border)
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()
-- that dropped the unit filters along with the registrations, so the cache
-- has to go too or the next UpdateVisibility believes they are still set
f.eventunit = nil
f:Hide()
end
end
function pfUI.uf.OnShow()
pfUI.uf:RefreshUnit(this, "portrait")
pfUI.uf:RefreshUnit(this, "base")
end
-- GUID-keyed aura start-time tracker, fed by Nampower's BUFF_ADDED_OTHER /
-- BUFF_REMOVED_OTHER. Non-player units have aura.expirationTime == 0 (server
-- doesn't broadcast time), so we reconstruct the swirl from "when we saw it
-- applied" + aura.duration (Spell.dbc base). Only auras we actually witnessed
-- being cast get tracked — pre-existing auras (already on a unit when you
-- target them) intentionally show no timer rather than a fabricated one.
pfUI.uf.aura_starts = {} -- {[guid] = {[spellId] = startTime}}
local auraTracker = CreateFrame("Frame", "pfUF_AuraTracker", UIParent)
auraTracker:RegisterEvent("BUFF_ADDED_OTHER")
auraTracker:RegisterEvent("BUFF_REMOVED_OTHER")
auraTracker:SetScript("OnEvent", function()
-- Nampower arg layout: arg1=guid, arg3=spellId. State code 0=added, 1=removed,
-- 2=modified; we treat any added/modified as "reset start = now" and let the
-- REMOVED event clear the entry.
if not arg1 or not arg3 or arg3 == 0 then return end
if event == "BUFF_REMOVED_OTHER" then
if pfUI.uf.aura_starts[arg1] then
pfUI.uf.aura_starts[arg1][arg3] = nil
end
else
pfUI.uf.aura_starts[arg1] = pfUI.uf.aura_starts[arg1] or {}
pfUI.uf.aura_starts[arg1][arg3] = GetTime()
end
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)
-- visibilityscan is a separate frame and keeps ticking, so leaving this one
-- on its list would have UpdateVisibility re-register the unit events we
-- just dropped -- straight back into the crash 132 this branch prevents
visibilityscan.frames[this] = 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 = UnitGUID(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 == "PLAYER_EQUIPMENT_CHANGED") then
this.update_aura = true
elseif this.label == "pet" and event == "UNIT_HAPPINESS" then
this.update_full = true
-- UNIT_XXX Events. RegisterUnitEvents means arg1 can only be this frame's own
-- unit; the compare is kept for the case the filter sits out, which is when
-- arg1 is not a string. The old GUID alternative is gone: these events fire
-- once per token that resolves to the unit AND once with the raw GUID, so the
-- token form always arrives and the GUID form was only ever a duplicate wake.
elseif arg1 and arg1 == this.unitstr then
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
-- ============================================================================
-- OnUpdate - eventless per-frame work (range check, aggro glow) and draining
-- the event-set update flags. Frames refresh on events only; no polling.
-- ============================================================================
function pfUI.uf.OnUpdate()
local now = _GetTime()
pfUI.uf.now = now
-- 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
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)
local health, maxHealth = UnitHealth(unit), UnitHealthMax(unit)
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)
this.ressIcon:SetShown(ress and UnitIsDeadOrGhost(unit))
end
end
end
-- ============================================================================
-- EVENT-BASED UPDATES (Health, Mana, Auras, etc.)
-- ============================================================================
-- 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
if not this.label or 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
-- The unit events whose arg1 is the frame's OWN unit -- everything the OnEvent
-- routes through its "UNIT_XXX Events" branch. These are not registered here:
-- UpdateVisibility owns them, because it owns the frame's unit (below).
--
-- UNIT_PET and UNIT_HAPPINESS are deliberately absent. Their branches key on
-- the frame's label, and UNIT_PET's arg1 is the pet's OWNER ("player" for a
-- "pet" frame), so filtering them by the frame's own unit would drop them.
local UNIT_EVENTS = {
"UNIT_DISPLAYPOWER",
"UNIT_HEALTH", "UNIT_MAXHEALTH",
"UNIT_MANA", "UNIT_MAXMANA",
"UNIT_RAGE", "UNIT_MAXRAGE",
"UNIT_ENERGY", "UNIT_MAXENERGY",
"UNIT_FOCUS",
"UNIT_PORTRAIT_UPDATE", "UNIT_MODEL_CHANGED",
"UNIT_FACTION",
"UNIT_AURA", -- frame=buff, frame=debuff
}
-- Point this frame's unit-event subscriptions at `unitstr`, or drop them when
-- the frame has no unit. Cheap to call repeatedly: it no-ops unless the unit
-- actually changed, which matters because visibilityscan runs UpdateVisibility
-- for every frame five times a second.
function pfUI.uf:RegisterUnitEvents(unitstr)
if self.eventunit == unitstr then return end
self.eventunit = unitstr
for i = 1, table.getn(UNIT_EVENTS) do
if unitstr then
self:RegisterUnitEvent(UNIT_EVENTS[i], unitstr)
else
self:UnregisterEvent(UNIT_EVENTS[i])
end
end
-- UNIT_COMBAT rides along only while the frame draws combat feedback text.
-- UpdateConfig clears eventunit when it toggles that, so the next
-- UpdateVisibility re-runs this.
if unitstr and self.combatfeedback then
self:RegisterUnitEvent("UNIT_COMBAT", unitstr)
else
self:UnregisterEvent("UNIT_COMBAT")
end
end
function pfUI.uf:EnableEvents()
local f = self
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_LOGOUT")
f:RegisterEvent("PLAYER_AURAS_CHANGED") -- label=player && frame=buff
f:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") -- label=player && frame=buff (ClassicAPI: weapon-enchant buffs)
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
-- ============================================================
-- Clique compatibility
-- Clique's pfUI plugin replaces pfUI.uf.ClickAction and expects an
-- OnClick script to call it (with `this` = frame, `arg1` = button). The
-- secure attribute path replaced that dispatch, so when Clique is loaded
-- EnableScripts routes clicks through this legacy Lua path instead. The
-- secure path is skipped there because type1="target" switches the target
-- before Clique can cast.
-- ============================================================
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:ClickAction(button)
local label = this.label or ""
local id = this.id or ""
local unitstr = label .. id
if SpellIsTargeting() and button == "RightButton" then
SpellStopTargeting()
return
end
if SpellIsTargeting() and button == "LeftButton" then
SpellTargetUnit(unitstr)
elseif CursorHasItem() then
DropItemOnUnit(unitstr)
end
-- right-click opens the standard unit menu (ClassicAPI resolves the type)
if button == "RightButton" then
ClassicAPI_ToggleUnitMenu(unitstr)
return
end
-- drop food on petframe
if label == "pet" and CursorHasItem() then
if UnitClassBase("player") == "HUNTER" then
DropItemOnUnit("pet")
return
end
end
-- default click
TargetUnit(unitstr)
end
function pfUI.uf:EnableScripts()
local f = self
if IsAddOnLoaded("Clique") then
f:SetScript("OnClick", pfUI.uf.OnClick)
else
f:SetAttribute("type1", "target")
f:SetAttribute("type2", "menu")
f:EnableClickCast()
end
f:SetScript("OnShow", pfUI.uf.OnShow)
f:SetScript("OnEvent", pfUI.uf.OnEvent)
f:SetScript("OnUpdate", pfUI.uf.OnUpdate)
-- 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)
-- 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.RegisterUnitEvents = pfUI.uf.RegisterUnitEvents
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)
-- Druid secondary mana bar: shows base mana (read via ClassicAPI
-- UnitPower(unit, Enum.PowerType.Mana), which works regardless of the active
-- power) while shapeshifted into a form that uses energy/rage. Player +
-- target only;
-- styled/positioned in UpdateConfig, driven in UpdateDruidMana.
if C.unitframes.druidmanabar == "1" and (f.label == "player" or f.label == "target") then
f.druidmana = CreateFrame("StatusBar", "pfDruidMana_" .. f.label .. f.id, f)
f.druidmana.text = f.druidmana:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
f.druidmana.text:SetPoint("CENTER", f.druidmana, "CENTER", 0, 0)
f.druidmana.text:SetJustifyH("CENTER")
f.druidmana:Hide()
end
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(T["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()
-- that dropped the unit filters along with the registrations, so the cache
-- has to go too or the next UpdateVisibility believes they are still set
f.eventunit = nil
f:Hide()
end
if f.label ~= "" then
f:SetAttribute("unit", f.label .. f.id)
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
unit.leaderIcon:SetShown(unit.config.leadericon == "1" and UnitIsPartyLeader(unitstr) and IsInGroup())
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
unit.lootIcon:SetShown(name and name == UnitName(unitstr))
end
end
if unit.pvpIcon then -- PvP Icon
unit.pvpIcon:SetShown(unit.config.showPVP == "1" and UnitIsPVP(unitstr))
end
if unit.restIcon and unit:GetName() == "pfPlayer" then -- Rest Icon
unit.restIcon:SetShown(C.unitframes.player.showRest == "1" and UnitIsUnit(unitstr, "player") and IsResting())
end
if unit.happinessIcon and unit:GetName() == "pfPet" then -- Happiness Icon
local pclass = UnitClassBase("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
-- Druid secondary mana bar update. Reads base mana with ClassicAPI
-- UnitPower/UnitPowerMax(unit, Enum.PowerType.Mana), which return the mana
-- slot regardless of the unit's active power -- so it works while shapeshifted,
-- with no nampower/GetUnitField dependency. Shown only while off mana
-- (Cat=energy, Bear=rage); non-player frames only for druid units.
function pfUI.uf:UpdateDruidMana(unit)
local bar = unit.druidmana
local unitstr = unit.label .. unit.id
if not UnitExists(unitstr) then bar:Hide() return end
if unit.label ~= "player" then
local cls = UnitClassBase(unitstr)
if cls ~= "DRUID" then bar:Hide() return end
end
if UnitPowerType(unitstr) == Enum.PowerType.Mana then bar:Hide() return end
local mana, maxmana = UnitPower(unitstr, Enum.PowerType.Mana), UnitPowerMax(unitstr, Enum.PowerType.Mana)
if not maxmana or maxmana == 0 then bar:Hide() return end
bar:SetMinMaxValues(0, maxmana)
bar:SetValue(mana)
if C.unitframes.druidmanatext == "1" then
bar.text:SetText(pfUI.api.Abbreviate(mana) .. "/" .. pfUI.api.Abbreviate(maxmana))
else
bar.text:SetText("")
end
bar:Show()
end
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
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
-- one GetAuraSlots enumeration per refresh, then a positional read per
-- slot id: allocates nothing and never re-walks the aura array per icon
ScanAuraSlots(unitstr, "HELPFUL", auraSlots, unit.config.bufflimit)
for i=1, unit.config.bufflimit do
if not unit.buffs[i] then break end
local name, icon, count, _, duration, expirationTime, _, _, _, spellId = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
if name then
unit.buffs[i].texture:SetTexture(icon)
unit.buffs[i]:Show()
if count > 1 then
unit.buffs[i].stacks:SetText(count)
else
unit.buffs[i].stacks:SetText("")
end
if expirationTime > 0 then
-- pfUI's cooldown text falls into a 2^32 wraparound branch when start > GetTime(),
-- which happens for talent-extended buffs where the real duration exceeds the
-- Spell.dbc base. Anchor start to now in that case to keep the remaining math sane.
local now = GetTime()
local start = expirationTime - duration
local dur = duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if dur > 0 then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, dur, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
elseif duration > 0 then
local guid = UnitGUID(unitstr)
local guidStarts = guid and pfUI.uf.aura_starts[guid]
local start = guidStarts and guidStarts[spellId]
if start then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, duration, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
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
-- selfdebuff narrows to player-cast harmful auras via the PLAYER filter.
-- Player-frame debuffs aren't gated on it (it'd hide most party-applied
-- effects on you). One GetAuraSlots enumeration per refresh; the i-th slot
-- is the i-th aura of the filtered list, so `i` stays the tooltip index.
local filter = (unit.label ~= "player" and selfdebuff == "1") and "HARMFUL|PLAYER" or "HARMFUL"
ScanAuraSlots(unitstr, filter, auraSlots, unit.config.debufflimit)
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
-- positional read by slot id allocates nothing
local name, icon, count, dispelType, duration, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
if name then
texture, stacks, dtype = icon, count, dispelType
else
texture, stacks, dtype = nil, 0, nil
end
unit.debuffs[i].texture:SetTexture(texture)
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(dtype or "")
unit.debuffs[i].backdrop:SetBackdropBorderColor(dispelColor:GetRGBA())
if texture then
unit.debuffs[i]:Show()
if expirationTime > 0 then
-- Cap start to now so talent-extended debuffs (expirationTime past
-- the dbc base duration) don't push start into the future and trip
-- CooldownFrame_SetTimer's 2^32-wraparound branch.
local now = GetTime()
local start = expirationTime - duration
local dur = duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if dur > 0 then
CooldownFrame_SetTimer(unit.debuffs[i].cd, start, dur, 1)
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
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:SetSize(size, 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
-- Scan the 16 harmful slots once into a reusable set, instead of
-- re-scanning all 16 for every dispellable type below. Reused across
-- frames (populated and consumed within this single RefreshUnit call).
local present = pfUI.uf.dispelPresent or {}
pfUI.uf.dispelPresent = present
for k in pairs(present) do present[k] = nil end
local n = ScanAuraSlots(unitstr, "HARMFUL", auraSlots)
for i=1,n do
local name, _, _, dispelType = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
if name and dispelType and dispelType ~= "" then present[dispelType] = true 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
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(debuff)
if disptype == "4" then
indicator[debuff].tex:SetTexture(pfUI.media["img:"..debuff])
indicator[debuff].tex:SetVertexColor(dispelColor:GetRGBA())
indicator[debuff].tex:Show()
indicator[debuff]:ClearAllPoints()
indicator[debuff]:SetSize(size, size)
indicator[debuff]:SetBackdrop(nil)
elseif disptype == "3" then
indicator[debuff].tex:SetTexture(dispelColor:GetRGBA())
indicator[debuff].tex:SetVertexColor(1,1,1,1)
indicator[debuff].tex:Show()
indicator[debuff]:ClearAllPoints()
indicator[debuff]:SetSize(size, 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(dispelColor:GetRGBA())
elseif disptype == "1" then
indicator[debuff].tex:SetTexture(dispelColor:GetRGBA())
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 = present[debuff]
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] = v:lower()
end
elseif not unit.indicator_custom then
unit.indicator_custom = {}
end
local pos = 1
if table.getn(unit.indicators) > 0 then
local n = ScanAuraSlots(unitstr, "HELPFUL", auraSlots)
for i=1,n do
local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
if not name then break end
local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil
for _, filter in pairs(unit.indicators) do
if filter.icon == icon and filter.name == name then
if filter.predict then
local start, duration, prediction = libpredict:GetHotDuration(unitstr, filter.predict)
pfUI.uf:AddIcon(unit, pos, icon, timeleft or prediction, count, tonumber(start), tonumber(duration))
else
pfUI.uf:AddIcon(unit, pos, icon, timeleft, count)
end
pos = pos + 1
break
end
end
end
end
if table.getn(unit.indicator_custom) > 0 then
local n = ScanAuraSlots(unitstr, "HELPFUL", auraSlots)
for i=1,n do
local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
if not name then break end
local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil
local lowerName = name:lower()
for _, filter in pairs(unit.indicator_custom) do
if filter == lowerName then
pfUI.uf:AddIcon(unit, pos, icon, timeleft, count)
pos = pos + 1
break
end
end
end
local debuffFilter = unit.config.selfdebuff == "1" and "HARMFUL|PLAYER" or "HARMFUL"
n = ScanAuraSlots(unitstr, debuffFilter, auraSlots)
for i=1,n do -- scan for custom debuffs
local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i])
if name then
local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil
for _, filter in pairs(unit.indicator_custom) do
if filter == name:lower() then
pfUI.uf:AddIcon(unit, pos, icon, timeleft, count)
pos = pos + 1
break
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)
-- 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
_, r, g, b = GetUnitColor(unitstr)
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 == Enum.PowerType.Mana then
r, g, b, a = GetStringColor(mana)
elseif utype == Enum.PowerType.Rage then
r, g, b, a = GetStringColor(rage)
elseif utype == Enum.PowerType.Focus then
r, g, b, a = GetStringColor(focus)
elseif utype == Enum.PowerType.Energy 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
if unit.druidmana then pfUI.uf:UpdateDruidMana(unit) end
pfUI.uf:RefreshUnitState(unit)
end
end
local modifiers = {
[""] = "",
["alt"] = "_alt",
["ctrl"] = "_ctrl",
["shift"] = "_shift",
}
function pfUI.uf:EnableClickCast()
if self.config.clickcast ~= "1" then return end
for bid = 1, 5 do -- LeftButton, RightButton, MiddleButton, Button4, Button5
for modifier, mconf in pairs(modifiers) do
local bconf = bid == 1 and "" or bid
local action = pfUI_config.unitframes["clickcast"..bconf..mconf]
if action and action ~= "" then
local prefix = modifier ~= "" and (modifier .. "-") or ""
local low = action:lower()
if low == "menu" then
self:SetAttribute(prefix .. "type" .. bid, "menu")
elseif low == "target" then
self:SetAttribute(prefix .. "type" .. bid, "target")
elseif low == "focus" then
self:SetAttribute(prefix .. "type" .. bid, "focus")
elseif low:find("^macro:") then
self:SetAttribute(prefix .. "type" .. bid, "macro")
self:SetAttribute(prefix .. "macro" .. bid, action:sub(7):gsub("^%s+", ""))
elseif action:find("^/") then
self:SetAttribute(prefix .. "type" .. bid, "macro")
self:SetAttribute(prefix .. "macrotext" .. bid, action)
else
self:SetAttribute(prefix .. "type" .. bid, "spell")
self:SetAttribute(prefix .. "spell" .. bid, action)
end
end
end
end
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("Model", 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]:SetSize(iconsize, 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 = UnitClassBase("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 = UnitClassBase("player")
local indicators = {}
if config.show_buffs == "1" then -- buffs
if myclass == "DRUID" then
AddIndicator(indicators, 1126) -- Mark of the Wild
AddIndicator(indicators, 21849) -- Gift of the Wild
AddIndicator(indicators, 467) -- Thorns
end
if myclass == "PRIEST" then
AddIndicator(indicators, 1243) -- Power Word: Fortitude
AddIndicator(indicators, 21562) -- Prayer of Fortitude
AddIndicator(indicators, 6386) -- Divine Spirit
AddIndicator(indicators, 27681) -- Prayer of Spirit
AddIndicator(indicators, 976) -- Shadow Protection
AddIndicator(indicators, 27683) -- Prayer of Shadow Protection
AddIndicator(indicators, 6346) -- Fear Ward
end
if myclass == "PALADIN" then
AddIndicator(indicators, 1038) -- Blessing of Salvation
AddIndicator(indicators, 25895) -- Greater Blessing of Salvation
AddIndicator(indicators, 19742) -- Blessing of Wisdom
AddIndicator(indicators, 25894) -- Greater Blessing of Wisdom
AddIndicator(indicators, 20204) -- Blessing of Sanctuary
AddIndicator(indicators, 25899) -- Greater Blessing of Sanctuary
AddIndicator(indicators, 20217) -- Blessing of Kings
AddIndicator(indicators, 25898) -- Greater Blessing of Kings
AddIndicator(indicators, 19740) -- Blessing of Might
AddIndicator(indicators, 25782) -- Greater Blessing of Might
AddIndicator(indicators, 19977) -- Blessing of Light
AddIndicator(indicators, 25890) -- Greater Blessing of Light
AddIndicator(indicators, 6940) -- Hand of Sacrifice
AddIndicator(indicators, 45801) -- Greater Blessing of Sacrifice
AddIndicator(indicators, 1044) -- Hand of Freedom
AddIndicator(indicators, 1022) -- Hand of Protection
end
if myclass == "WARLOCK" then
AddIndicator(indicators, 1167) -- Fire Shield
AddIndicator(indicators, 6307) -- Blood Pact
AddIndicator(indicators, 20707) -- Soulstone Resurrection
AddIndicator(indicators, 5697) -- Unending Breath
AddIndicator(indicators, 2970) -- Detect Invisibility
AddIndicator(indicators, 11743) -- Detect Greater Invisibility
AddIndicator(indicators, 132) -- Detect Lesser Invisibility
AddIndicator(indicators, 19480) -- Paranoia
end
if myclass == "WARRIOR" then
AddIndicator(indicators, 5242) -- Battle Shout
AddIndicator(indicators, 45580) -- Commanding Shout
end
if myclass == "MAGE" then
AddIndicator(indicators, 1459) -- Arcane Intellect
AddIndicator(indicators, 23028) -- Arcane Brilliance
AddIndicator(indicators, 604) -- Dampen Magic
AddIndicator(indicators, 1008) -- Amplify Magic
end
if myclass == "HUNTER" then
AddIndicator(indicators, 20043) -- Aspect of the Wild
AddIndicator(indicators, 13159) -- Aspect of the Pack
end
if myclass == "SHAMAN" then
AddIndicator(indicators, 45525) -- Earth Shield
end
end
if config.show_procs == "1" then -- procs
if myclass == "SHAMAN" or config.all_procs == "1" then
AddIndicator(indicators, 16177) -- Ancestral Fortitude
AddIndicator(indicators, 29202) -- Healing Way
-- Totemic Power is four auras, one per totem school, each with its own icon
AddIndicator(indicators, 28824)
AddIndicator(indicators, 28825)
AddIndicator(indicators, 28826)
AddIndicator(indicators, 28827)
end
if myclass == "PRIEST" or config.all_procs == "1" then
AddIndicator(indicators, 14893) -- Inspiration
end
end
if config.show_hots == "1" then -- hots
if myclass == "PRIEST" or config.all_hots == "1" then
AddIndicator(indicators, 139, "Renew") -- Renew
AddIndicator(indicators, 17) -- Power Word: Shield
end
if myclass == "DRUID" or config.all_hots == "1" then
AddIndicator(indicators, 8936, "Regr") -- Regrowth
AddIndicator(indicators, 774, "Reju") -- Rejuvenation
end
end
if config.show_totems == "1" and myclass == "SHAMAN" then -- totems
-- the aura each totem applies, not the cast that drops it: they share an
-- icon but the totem's own name carries a " Totem" suffix the aura lacks
AddIndicator(indicators, 8076) -- Strength of Earth
AddIndicator(indicators, 8072) -- Stoneskin
AddIndicator(indicators, 5677) -- Mana Spring
AddIndicator(indicators, 16191) -- Mana Tide
AddIndicator(indicators, 5672) -- Healing Stream
AddIndicator(indicators, 25909) -- Tranquil Air
AddIndicator(indicators, 8836) -- Grace of Air
AddIndicator(indicators, 8177) -- Grounding Totem
AddIndicator(indicators, 10596) -- Nature Resistance
AddIndicator(indicators, 8185) -- Fire Resistance
AddIndicator(indicators, 8182) -- Frost Resistance
end
return indicators
end
local function abbrevname(t)
return t:sub(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 = name:gsub("^(%S+) ", abbrevname)
end
-- abbreviate all if it still doesn't fit
if abbrev and name and strlen(name) > size then
name = name:gsub("(%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)
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)
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
elseif config == "ownername" then
local owner
if unit.label == "raidpet" then owner = "raid" .. unit.id
elseif unit.label == "partypet" then owner = "party" .. unit.id
elseif unit.label == "pet" then owner = "player" end
if not (owner and UnitExists(owner)) then return "" end
local color = ""
if unit.config["classcolor"] == "1" and UnitIsPlayer(owner) then
local _, r, g, b = GetUnitColor(owner)
if C.unitframes.pastel == "1" then r, g, b = (r+.75)*.5, (g+.75)*.5, (b+.75)*.5 end
color = rgbhex(r, g, b)
end
return color .. pfUI.uf:GetNameString(owner)
-- 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 = UnitPowerMax(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) == Enum.PowerType.Mana 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
_, r, g, b = GetUnitColor(unitstr)
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
_, r, g, b = GetUnitColor(unitstr)
elseif preset == "reaction" and config["classcolor"] == "1" then
local color = UnitReactionColor[UnitReaction(unitstr, "player")]
r, g, b = color.r, color.g, color.b
elseif preset == "health" and config["healthcolor"] == "1" then
local hp, hpmax = UnitHealth(unitstr), UnitHealthMax(unitstr)
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
local color = ManaBarColor[UnitPowerType(unitstr)]
r, g, b = color.r, color.g, color.b
elseif preset == "level" and config["levelcolor"] == "1" then
local color = GetDifficultyColor(UnitLevel(unitstr))
r, g, b = color.r, color.g, color.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