Restore Real Health Numbers and fix aura dragging

Restore Real Health Numbers as a standalone module with a lazy Vanilla health estimator fallback, keep Big Health presentation-only, clarify the module tooltip, and fix aura dragging for BuffButton0/BuffButton32/TempEnchant1 without StartMoving().
This commit is contained in:
Dusk-92
2026-08-31 11:10:03 +02:00
committed by GitHub
parent 6d59f60c27
commit ccfeb0810f
6 changed files with 583 additions and 164 deletions
+279
View File
@@ -0,0 +1,279 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local Abbreviate = ShaguTweaks.Abbreviate
local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Real Health Numbers"],
description = T["Shows health and power numbers on player, pet and target unit frames. On Vanilla servers, target health may be estimated when exact values are unavailable."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
enabled = nil,
})
module.enable = function(self)
if ShaguTweaks.RealHealthNumbersInstalled then return end
ShaguTweaks.RealHealthNumbersInstalled = true
if ShaguTweaks.libhealth and ShaguTweaks.libhealth.Enable then
ShaguTweaks.libhealth:Enable()
end
local function HideNativeText(text)
if text and text.SetAlpha then
text:SetAlpha(0)
end
end
local nativeTexts = {
PlayerFrameHealthBar and PlayerFrameHealthBar.TextString,
PlayerFrameManaBar and PlayerFrameManaBar.TextString,
TargetFrameHealthBar and TargetFrameHealthBar.TextString,
TargetFrameManaBar and TargetFrameManaBar.TextString,
PetFrameHealthBar and PetFrameHealthBar.TextString,
PetFrameManaBar and PetFrameManaBar.TextString,
_G.PlayerFrameAlternatePowerBarText,
_G.TargetHPText,
_G.TargetHPPercText,
}
for _, text in pairs(nativeTexts) do
HideNativeText(text)
end
self.valueTexts = self.valueTexts or {}
local function CreateValueText(parent, fontSize)
if not parent then return end
local text = parent:CreateFontString(nil, "OVERLAY", "GameFontWhite")
text:SetFont(STANDARD_TEXT_FONT, fontSize or 10, "OUTLINE")
text:SetHeight(32)
text:SetJustifyH("CENTER")
text:SetTextColor(1, 1, 1, 1)
return text
end
local function AnchorText(text, bar, y)
if not text or not bar then return end
text:ClearAllPoints()
text:SetPoint("CENTER", bar, "CENTER", 0, y or 0)
end
if not self.valueTexts.playerHealth then
self.valueTexts.playerHealth = CreateValueText(PlayerFrameHealthBar, 10)
self.valueTexts.playerMana = CreateValueText(PlayerFrameManaBar, 10)
self.valueTexts.targetHealth = CreateValueText(TargetFrameHealthBar, 10)
self.valueTexts.targetMana = CreateValueText(TargetFrameManaBar, 10)
if PetFrameHealthBar then
self.valueTexts.petHealth = CreateValueText(PetFrameHealthBar, 9)
end
if PetFrameManaBar then
self.valueTexts.petMana = CreateValueText(PetFrameManaBar, 9)
end
if _G.PlayerFrameAlternatePowerBar then
self.valueTexts.playerAlternatePower =
CreateValueText(_G.PlayerFrameAlternatePowerBar, 10)
end
end
local function PositionTexts()
local playerHealthY = PlayerFrameHealthBar:GetHeight() >= 20 and -7 or 0
local targetHealthY = TargetFrameHealthBar:GetHeight() >= 20 and -7 or 0
AnchorText(self.valueTexts.playerHealth, PlayerFrameHealthBar, playerHealthY)
AnchorText(self.valueTexts.playerMana, PlayerFrameManaBar, 0)
AnchorText(self.valueTexts.targetHealth, TargetFrameHealthBar, targetHealthY)
AnchorText(self.valueTexts.targetMana, TargetFrameManaBar, 0)
AnchorText(self.valueTexts.petHealth, PetFrameHealthBar, 0)
AnchorText(self.valueTexts.petMana, PetFrameManaBar, -2)
if _G.PlayerFrameAlternatePowerBar then
AnchorText(
self.valueTexts.playerAlternatePower,
_G.PlayerFrameAlternatePowerBar,
0
)
end
end
local function SetValue(text, value)
if not text then return end
if value == nil then
text:SetText("")
text:Hide()
return
end
text:SetText(tostring(Abbreviate(value)))
text:Show()
end
local function UpdateHealth(text, unit, useEstimator)
if not text then return end
if not UnitExists(unit) then
text:SetText("")
text:Hide()
return
end
if unit == "target" and (UnitIsDead("target") or UnitIsGhost("target")) then
text:SetText("")
text:Hide()
return
end
local cur = UnitHealth(unit)
local max = UnitHealthMax(unit)
local known = true
if useEstimator and ShaguTweaks.libhealth
and ShaguTweaks.libhealth.GetUnitHealth then
cur, max, known = ShaguTweaks.libhealth:GetUnitHealth(unit)
end
if not max or max <= 0 then
text:SetText("")
text:Hide()
return
end
if useEstimator and not known and max == 100 then
text:SetText(tostring(cur or 0) .. "%")
text:Show()
return
end
SetValue(text, cur or 0)
end
local function UpdatePower(text, unit)
if not text then return end
if not UnitExists(unit) then
text:SetText("")
text:Hide()
return
end
local max = UnitManaMax(unit)
if not max or max <= 0 then
text:SetText("")
text:Hide()
return
end
SetValue(text, UnitMana(unit) or 0)
end
local function UpdatePlayer()
UpdateHealth(self.valueTexts.playerHealth, "player", false)
UpdatePower(self.valueTexts.playerMana, "player")
end
local function UpdateTarget()
UpdateHealth(self.valueTexts.targetHealth, "target", true)
UpdatePower(self.valueTexts.targetMana, "target")
end
local function UpdatePet()
UpdateHealth(self.valueTexts.petHealth, "pet", false)
UpdatePower(self.valueTexts.petMana, "pet")
end
local function UpdateAlternatePower()
local bar = _G.PlayerFrameAlternatePowerBar
local text = self.valueTexts.playerAlternatePower
if not bar or not text then return end
if not bar:IsShown() then
text:SetText("")
text:Hide()
return
end
local _, max = bar:GetMinMaxValues()
if not max or max <= 0 then
text:SetText("")
text:Hide()
return
end
local value = bar:GetValue() or 0
SetValue(text, math.floor(value + 0.5))
end
self.valueEvents = self.valueEvents or CreateFrame("Frame")
local events = self.valueEvents
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("PLAYER_TARGET_CHANGED")
events:RegisterEvent("PET_UI_UPDATE")
events:RegisterEvent("UNIT_HEALTH")
events:RegisterEvent("UNIT_MAXHEALTH")
events:RegisterEvent("UNIT_MANA")
events:RegisterEvent("UNIT_MAXMANA")
events:RegisterEvent("UNIT_RAGE")
events:RegisterEvent("UNIT_MAXRAGE")
events:RegisterEvent("UNIT_ENERGY")
events:RegisterEvent("UNIT_MAXENERGY")
events:RegisterEvent("UNIT_FOCUS")
events:RegisterEvent("UNIT_MAXFOCUS")
events:RegisterEvent("UNIT_HAPPINESS")
events:RegisterEvent("UNIT_MAXHAPPINESS")
events:RegisterEvent("UNIT_DISPLAYPOWER")
events:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
PositionTexts()
UpdatePlayer()
UpdateTarget()
UpdatePet()
UpdateAlternatePower()
elseif event == "PLAYER_TARGET_CHANGED" then
UpdateTarget()
elseif event == "PET_UI_UPDATE" then
UpdatePet()
elseif arg1 == "player" then
UpdatePlayer()
UpdateAlternatePower()
elseif arg1 == "target" then
UpdateTarget()
elseif arg1 == "pet" then
UpdatePet()
end
end)
local alternatePowerBar = _G.PlayerFrameAlternatePowerBar
if alternatePowerBar and self.valueTexts.playerAlternatePower then
hooksecurefunc(alternatePowerBar, "SetValue", UpdateAlternatePower)
hooksecurefunc(alternatePowerBar, "Show", UpdateAlternatePower)
hooksecurefunc(alternatePowerBar, "Hide", UpdateAlternatePower)
end
-- All enabled modules are initialized during VARIABLES_LOADED in an
-- unspecified table order. Reposition once on the next frame so this module
-- automatically fits either the stock bars or Unit Frame Big Health.
local deferred = CreateFrame("Frame")
deferred:SetScript("OnUpdate", function()
this:SetScript("OnUpdate", nil)
this:Hide()
PositionTexts()
for _, text in pairs(nativeTexts) do
HideNativeText(text)
end
UpdatePlayer()
UpdateTarget()
UpdatePet()
UpdateAlternatePower()
end)
end
+86 -8
View File
@@ -63,9 +63,9 @@ module.enable = function(self)
{ name = "PartyMemberFrame3", persist = true },
{ name = "PartyMemberFrame4", persist = true },
{ name = "Minimap", moveParent = true, persist = true },
{ name = "BuffButton0", persist = true, manualGroup = "buffs" },
{ name = "BuffButton32", persist = true, manualGroup = "debuffs" },
{ name = "TempEnchant1", persist = true, manualGroup = "weapon" },
{ name = "BuffButton0", persist = true, manualGroup = "buffs", cursorDrag = true },
{ name = "BuffButton32", persist = true, manualGroup = "debuffs", cursorDrag = true },
{ name = "TempEnchant1", persist = true, manualGroup = "weapon", cursorDrag = true },
}
local function Resolve(target)
@@ -131,6 +131,65 @@ module.enable = function(self)
moveFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
end
-- Aura buttons are layout-owned frames on some Vanilla/Turtle UI builds.
-- Calling StartMoving() on them can fail with "Frame ... is not movable or
-- resizable" even after SetMovable(true). For those frames, follow the cursor
-- ourselves and only change their anchor; regular unit frames still use the
-- native StartMoving path.
local cursorDrag = CreateFrame("Frame")
cursorDrag:Hide()
local function CursorPosition()
local x, y = GetCursorPosition()
local scale = UIParent:GetEffectiveScale()
if not scale or scale <= 0 then scale = 1 end
return x / scale, y / scale
end
local function StartCursorDrag(state, target, moveFrame)
local left = moveFrame:GetLeft()
local top = moveFrame:GetTop()
if not left or not top then return false end
local x, y = CursorPosition()
state.cursorStartX = x
state.cursorStartY = y
state.frameStartLeft = left
state.frameStartTop = top
cursorDrag.state = state
cursorDrag.target = target
cursorDrag.moveFrame = moveFrame
cursorDrag:Show()
return true
end
local function StopCursorDrag(state)
if cursorDrag.state ~= state then return end
cursorDrag:Hide()
cursorDrag.state = nil
cursorDrag.target = nil
cursorDrag.moveFrame = nil
end
cursorDrag:SetScript("OnUpdate", function()
local state = this.state
local target = this.target
local moveFrame = this.moveFrame
if not state or not target or not moveFrame then
this:Hide()
return
end
local x, y = CursorPosition()
local left = state.frameStartLeft + (x - state.cursorStartX)
local top = state.frameStartTop + (y - state.cursorStartY)
moveFrame:ClearAllPoints()
moveFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
end)
local function CreateGrid()
if grid then return grid end
@@ -217,7 +276,11 @@ module.enable = function(self)
moveFrame:SetClampedToScreen(true)
end
moveFrame:SetMovable(true)
-- Aura buttons can reject StartMoving() on some clients. They only need
-- mouse/drag scripts because cursorDrag handles their position directly.
if not target.cursorDrag then
moveFrame:SetMovable(true)
end
handle:EnableMouse(true)
handle:RegisterForDrag("LeftButton")
@@ -230,11 +293,22 @@ module.enable = function(self)
moveFrame:SetUserPlaced(true)
end
moveFrame:StartMoving()
if target.cursorDrag then
if not StartCursorDrag(state, target, moveFrame) then
state.dragged = false
SetDragging(target, false)
end
else
moveFrame:StartMoving()
end
end)
handle:SetScript("OnDragStop", function()
moveFrame:StopMovingOrSizing()
if target.cursorDrag then
StopCursorDrag(state)
else
moveFrame:StopMovingOrSizing()
end
if state.dragged then
SavePosition(target, moveFrame)
@@ -252,14 +326,18 @@ module.enable = function(self)
local moveFrame = state.moveFrame
if moveFrame then
moveFrame:StopMovingOrSizing()
if target.cursorDrag then
StopCursorDrag(state)
else
moveFrame:StopMovingOrSizing()
end
SetDragging(target, false)
if state.dragged then
SavePosition(target, moveFrame)
end
if state.movable ~= nil then
if not target.cursorDrag and state.movable ~= nil then
moveFrame:SetMovable(state.movable)
end
+6 -155
View File
@@ -26,6 +26,8 @@ module.enable = function(self)
local eliteTexture = addonpath .. "\\img\\UI-TargetingFrame-Elite"
local rareTexture = addonpath .. "\\img\\UI-TargetingFrame-Rare"
-- Big Health is presentation-only. Numeric health/power values are owned by
-- the independent Real Health Numbers module.
PlayerFrameTexture:SetTexture(normalTexture)
PlayerFrameHealthBar:SetPoint("TOPLEFT", 106, -22)
PlayerFrameHealthBar:SetHeight(30)
@@ -36,8 +38,6 @@ module.enable = function(self)
TargetFrameHealthBar:SetPoint("TOPRIGHT", -106, -22)
TargetFrameHealthBar:SetHeight(30)
-- Dark mode is applied after the world has loaded. Keep this event separate
-- from the one-frame deferred setup so the event cannot be unregistered early.
local world = CreateFrame("Frame")
world:RegisterEvent("PLAYER_ENTERING_WORLD")
world:SetScript("OnEvent", function()
@@ -47,7 +47,7 @@ module.enable = function(self)
end)
-- Delay hook installation by one frame so all enabled unit-frame modules have
-- finished their setup first. This keeps hooks attached to the final handlers.
-- finished their setup first. This keeps hooks attached to final handlers.
local deferred = CreateFrame("Frame")
deferred:SetScript("OnUpdate", function()
this:SetScript("OnUpdate", nil)
@@ -55,7 +55,9 @@ module.enable = function(self)
local function UpdateTargetClassificationTexture()
local classification = UnitClassification("target")
if classification == "worldboss" or classification == "rareelite" or classification == "elite" then
if classification == "worldboss"
or classification == "rareelite"
or classification == "elite" then
TargetFrameTexture:SetTexture(eliteTexture)
elseif classification == "rare" then
TargetFrameTexture:SetTexture(rareTexture)
@@ -64,156 +66,8 @@ module.enable = function(self)
end
end
-- Let Blizzard and other addons finish classification handling, then apply
-- the matching Big Health texture instead of replacing the global function.
hooksecurefunc("TargetFrame_CheckClassification", UpdateTargetClassificationTexture)
-- Big Health owns the compact value display while enabled. Keep Blizzard /
-- Turtle status texts alive for compatibility, but make them invisible so
-- their current/max or percent formats cannot overlap our centered values.
local nativeTexts = {
PlayerFrameHealthBar.TextString,
PlayerFrameManaBar.TextString,
TargetFrameHealthBar.TextString,
TargetFrameManaBar.TextString,
_G.PlayerFrameAlternatePowerBarText,
_G.TargetHPText,
_G.TargetHPPercText,
}
for _, text in pairs(nativeTexts) do
if text then text:SetAlpha(0) end
end
if not self.valueTexts then
self.valueTexts = {}
local function CreateValueText(parent, anchor, y)
local text = parent:CreateFontString(nil, "OVERLAY", "GameFontWhite")
-- Match the compact unit-frame number style used by the former
-- health-numbers module: Friz Quadrata, 10px, outlined.
text:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
text:SetHeight(32)
text:ClearAllPoints()
text:SetPoint("CENTER", anchor, "CENTER", 0, y or 0)
text:SetJustifyH("CENTER")
text:SetTextColor(1, 1, 1, 1)
return text
end
self.valueTexts.playerHealth = CreateValueText(PlayerFrameHealthBar, PlayerFrameHealthBar, -7)
self.valueTexts.playerMana = CreateValueText(PlayerFrameManaBar, PlayerFrameManaBar, 0)
self.valueTexts.targetHealth = CreateValueText(TargetFrameHealthBar, TargetFrameHealthBar, -7)
self.valueTexts.targetMana = CreateValueText(TargetFrameManaBar, TargetFrameManaBar, 0)
if _G.PlayerFrameAlternatePowerBar then
self.valueTexts.playerAlternatePower = CreateValueText(
_G.PlayerFrameAlternatePowerBar,
_G.PlayerFrameAlternatePowerBar,
0
)
end
end
local function UpdateValueText(text, unit, power)
if unit == "target" and not UnitExists("target") then
text:Hide()
return
end
local value
local max
if power == "health" then
value = UnitHealth(unit)
max = UnitHealthMax(unit)
else
value = UnitMana(unit)
max = UnitManaMax(unit)
end
if not max or max <= 0 then
text:Hide()
return
end
text:SetText(tostring(value or 0))
text:Show()
end
local function UpdateUnitValues(unit)
if unit == "player" then
UpdateValueText(self.valueTexts.playerHealth, "player", "health")
UpdateValueText(self.valueTexts.playerMana, "player", "power")
elseif unit == "target" then
UpdateValueText(self.valueTexts.targetHealth, "target", "health")
UpdateValueText(self.valueTexts.targetMana, "target", "power")
end
end
if not self.valueEvents then
self.valueEvents = CreateFrame("Frame")
self.valueEvents:RegisterEvent("PLAYER_ENTERING_WORLD")
self.valueEvents:RegisterEvent("PLAYER_TARGET_CHANGED")
self.valueEvents:RegisterEvent("UNIT_HEALTH")
self.valueEvents:RegisterEvent("UNIT_MAXHEALTH")
self.valueEvents:RegisterEvent("UNIT_MANA")
self.valueEvents:RegisterEvent("UNIT_MAXMANA")
self.valueEvents:RegisterEvent("UNIT_RAGE")
self.valueEvents:RegisterEvent("UNIT_MAXRAGE")
self.valueEvents:RegisterEvent("UNIT_ENERGY")
self.valueEvents:RegisterEvent("UNIT_MAXENERGY")
self.valueEvents:RegisterEvent("UNIT_FOCUS")
self.valueEvents:RegisterEvent("UNIT_MAXFOCUS")
self.valueEvents:RegisterEvent("UNIT_HAPPINESS")
self.valueEvents:RegisterEvent("UNIT_MAXHAPPINESS")
self.valueEvents:RegisterEvent("UNIT_DISPLAYPOWER")
self.valueEvents:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_TARGET_CHANGED" then
UpdateUnitValues("player")
UpdateUnitValues("target")
elseif arg1 == "player" or arg1 == "target" then
UpdateUnitValues(arg1)
end
end)
end
UpdateUnitValues("player")
UpdateUnitValues("target")
-- Turtle WoW exposes a native alternate power bar for druids while
-- shapeshifted. It stores caster mana separately from the active rage/energy
-- value. Keep the native bar and only replace its text presentation.
local alternatePowerBar = _G.PlayerFrameAlternatePowerBar
if alternatePowerBar and self.valueTexts.playerAlternatePower then
local function UpdateAlternatePowerValue()
local text = self.valueTexts.playerAlternatePower
if not alternatePowerBar:IsShown() then
text:Hide()
return
end
local value = alternatePowerBar:GetValue() or 0
local _, max = alternatePowerBar:GetMinMaxValues()
if not max or max <= 0 then
text:Hide()
return
end
text:SetText(tostring(math.floor(value + 0.5)))
text:Show()
end
-- Turtle updates this bar from UnitFrameManaBar_Update using the second
-- UnitMana / UnitManaMax return values. Refresh after that native write.
hooksecurefunc(alternatePowerBar, "SetValue", UpdateAlternatePowerValue)
hooksecurefunc(alternatePowerBar, "Show", UpdateAlternatePowerValue)
UpdateAlternatePowerValue()
end
local playerSetStatusBarColor = PlayerFrameHealthBar.SetStatusBarColor
local targetSetStatusBarColor = TargetFrameHealthBar.SetStatusBarColor
@@ -249,11 +103,8 @@ module.enable = function(self)
ApplyTargetHealthColor()
end
-- Reapply the target health color after faction/class-color updates.
hooksecurefunc("TargetFrame_CheckFaction", ApplyTargetHealthColor)
-- Refresh once because the target frame may already have been initialized
-- before this deferred setup ran.
UpdateTargetClassificationTexture()
TargetFrame_CheckFaction()
end)