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:
@@ -34,6 +34,8 @@ Settings: **Esc → Advanced Options**.
|
||||
- Expanded Item Rarity Borders support for merchant, quest, mail, trade, crafting and loot frames.
|
||||
- Added independent Item Rarity Glows using the shared item rarity engine.
|
||||
- Expanded Movable Unit Frames to handle Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs through one shared Ctrl+Shift mover and alignment grid.
|
||||
- Restored Real Health Numbers as an independent module, with direct values when available and a lazy Vanilla health estimator fallback for percentage-only targets.
|
||||
- Unit Frame Big Health is presentation-only again and no longer forces numeric health/power text.
|
||||
- Various stability fixes across legacy ShaguTweaks modules.
|
||||
|
||||
## 🔷 Mods using ClassicAPI directly
|
||||
@@ -47,6 +49,7 @@ Settings: **Esc → Advanced Options**.
|
||||
- Item Rarity Borders
|
||||
- Item Rarity Glows
|
||||
- Movable Unit Frames
|
||||
- Real Health Numbers
|
||||
- Nameplate Class Colors
|
||||
- Nameplate Castbar
|
||||
- No Toggle
|
||||
@@ -79,6 +82,7 @@ Other modules can also benefit indirectly from ClassicAPI through shared ShaguTw
|
||||
- Energy & Mana Tick
|
||||
- Health Colors
|
||||
- Movable Unit Frames
|
||||
- Real Health Numbers
|
||||
- Enemy Target Castbar
|
||||
- Target Debuff Timer
|
||||
|
||||
@@ -146,7 +150,6 @@ Other modules can also benefit indirectly from ClassicAPI through shared ShaguTw
|
||||
The following original ShaguTweaks modules are not included as standalone modules because their functionality is either integrated elsewhere or no longer required:
|
||||
|
||||
- **Chat Links** → integrated into **Chat Tweaks**
|
||||
- **Real Health Numbers** → value display integrated into **Unit Frame Big Health**
|
||||
- **Blue Shaman Class Colors** → no longer required; supported natively by Turtle WoW-like clients
|
||||
- **Movable Unit Frames Extended** → integrated into **Movable Unit Frames**
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ libs\libdebuff.lua
|
||||
libs\libunitscan.lua
|
||||
libs\libnameplate.lua
|
||||
libs\libpredict.lua
|
||||
libs\libhealth.lua
|
||||
|
||||
# custom
|
||||
mods\custom.lua
|
||||
@@ -80,6 +81,7 @@ mods\chat-spam-filter.lua
|
||||
mods\hide-gryphons.lua
|
||||
mods\worldmap-colors.lua
|
||||
mods\unitframes-bighealth.lua
|
||||
mods\health-numbers.lua
|
||||
mods\unitframes-classportrait.lua
|
||||
mods\unitframes-energy-tick.lua
|
||||
mods\cooldown-numbers.lua
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
local _G = ShaguTweaks.GetGlobalEnv()
|
||||
|
||||
-- Real-health fallback for stock Vanilla servers.
|
||||
--
|
||||
-- Turtle WoW and some custom clients expose real UnitHealth/UnitHealthMax
|
||||
-- values directly. Stock 1.12 often exposes target health as 0..100 instead.
|
||||
-- Keep the original ShaguTweaks estimation idea, but leave it completely
|
||||
-- dormant until Real Health Numbers asks for it and only listen to combat/
|
||||
-- health events while the current target actually needs estimation.
|
||||
|
||||
local mobdb = {}
|
||||
local targetKey
|
||||
local damage = 0
|
||||
local percent = 0
|
||||
|
||||
local libhealth = CreateFrame("Frame")
|
||||
libhealth.enabled = false
|
||||
libhealth.reqhit = 2
|
||||
libhealth.reqdmg = 10
|
||||
|
||||
local function InitCache()
|
||||
ShaguTweaks_cache = ShaguTweaks_cache or {}
|
||||
ShaguTweaks_cache["libhealth"] = ShaguTweaks_cache["libhealth"] or {}
|
||||
mobdb = ShaguTweaks_cache["libhealth"]
|
||||
end
|
||||
|
||||
local function IsPercentHealth(unit)
|
||||
if not unit or not UnitExists(unit) then return false end
|
||||
|
||||
local cur = _G.UnitHealth(unit)
|
||||
local max = _G.UnitHealthMax(unit)
|
||||
|
||||
return cur ~= nil and max == 100 and cur <= 100
|
||||
end
|
||||
|
||||
local function StopSampling()
|
||||
libhealth:UnregisterEvent("UNIT_COMBAT")
|
||||
libhealth:UnregisterEvent("UNIT_HEALTH")
|
||||
end
|
||||
|
||||
local function StartSampling()
|
||||
libhealth:RegisterEvent("UNIT_COMBAT")
|
||||
libhealth:RegisterEvent("UNIT_HEALTH")
|
||||
end
|
||||
|
||||
local function SelectTarget()
|
||||
StopSampling()
|
||||
|
||||
damage = 0
|
||||
percent = _G.UnitHealth("target") or 0
|
||||
targetKey = nil
|
||||
|
||||
if not IsPercentHealth("target") then return end
|
||||
|
||||
local name = UnitName("target")
|
||||
local level = UnitLevel("target")
|
||||
if not name or not level then return end
|
||||
|
||||
targetKey = string.format("%s:%s", name, level)
|
||||
StartSampling()
|
||||
end
|
||||
|
||||
local function GetCachedHealth(unitstr, cur, max)
|
||||
local name = UnitName(unitstr)
|
||||
local level = UnitLevel(unitstr)
|
||||
if not name or not level then return cur, max end
|
||||
|
||||
local key = string.format("%s:%s", name, level)
|
||||
local data = mobdb[key]
|
||||
if data and data[1] and data[2]
|
||||
and data[2] > libhealth.reqdmg
|
||||
and (not data[3] or data[3] > libhealth.reqhit) then
|
||||
return ceil(data[1] / 100 * cur), data[1], true
|
||||
end
|
||||
|
||||
return cur, max
|
||||
end
|
||||
|
||||
-- ClassicAPI can expose the real health deficit even when stock UnitHealth()
|
||||
-- still reports a percentage. Use that as an immediate estimate when possible.
|
||||
-- At 100% there is no deficit to derive a maximum from, so the historical
|
||||
-- combat estimator/cache remains useful.
|
||||
local function GetMissingHealthEstimate(unitstr, cur, max)
|
||||
if max ~= 100 or cur >= 100 then return end
|
||||
if type(_G.UnitHealthMissing) ~= "function" then return end
|
||||
|
||||
local missing = _G.UnitHealthMissing(unitstr)
|
||||
local lostPercent = 100 - cur
|
||||
if not missing or missing <= 0 or lostPercent <= 0 then return end
|
||||
|
||||
local estimatedMax = ceil(missing / lostPercent * 100)
|
||||
if estimatedMax < 100 then return end
|
||||
|
||||
local estimatedCur = estimatedMax - missing
|
||||
if estimatedCur < 0 then estimatedCur = 0 end
|
||||
|
||||
return estimatedCur, estimatedMax, true
|
||||
end
|
||||
|
||||
function libhealth:GetUnitHealth(unitstr)
|
||||
-- Preserve the old public library behavior for external consumers while
|
||||
-- keeping the estimator lazy: the first actual query activates it.
|
||||
if not self.enabled then self:Enable() end
|
||||
|
||||
local cur = _G.UnitHealth(unitstr)
|
||||
local max = _G.UnitHealthMax(unitstr)
|
||||
|
||||
if not cur or not max then return cur or 0, max or 0 end
|
||||
|
||||
-- Real values are already available: do not estimate.
|
||||
if cur > 100 or max > 100 or max < 100 then
|
||||
return cur, max, true
|
||||
end
|
||||
|
||||
local missingCur, missingMax, missingKnown =
|
||||
GetMissingHealthEstimate(unitstr, cur, max)
|
||||
if missingKnown then
|
||||
return missingCur, missingMax, true
|
||||
end
|
||||
|
||||
return GetCachedHealth(unitstr, cur, max)
|
||||
end
|
||||
|
||||
function libhealth:GetUnitHealthByName(name, level, cur, max)
|
||||
if not self.enabled then self:Enable() end
|
||||
|
||||
if not cur or not max then return cur or 0, max or 0 end
|
||||
|
||||
if cur > 100 or max > 100 or max < 100 then
|
||||
return cur, max, true
|
||||
end
|
||||
|
||||
if not name or not level then return cur, max end
|
||||
|
||||
local key = string.format("%s:%s", name, level)
|
||||
local data = mobdb[key]
|
||||
if data and data[1] and data[2]
|
||||
and data[2] > libhealth.reqdmg
|
||||
and (not data[3] or data[3] > libhealth.reqhit) then
|
||||
return ceil(data[1] / 100 * cur), data[1], true
|
||||
end
|
||||
|
||||
return cur, max
|
||||
end
|
||||
|
||||
function libhealth:Enable()
|
||||
if self.enabled then return end
|
||||
self.enabled = true
|
||||
|
||||
InitCache()
|
||||
|
||||
self:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
self:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
SelectTarget()
|
||||
end
|
||||
|
||||
function libhealth:Disable()
|
||||
if not self.enabled then return end
|
||||
self.enabled = false
|
||||
|
||||
self:UnregisterAllEvents()
|
||||
targetKey = nil
|
||||
damage = 0
|
||||
percent = 0
|
||||
end
|
||||
|
||||
libhealth:SetScript("OnEvent", function()
|
||||
if not libhealth.enabled then return end
|
||||
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
InitCache()
|
||||
SelectTarget()
|
||||
elseif event == "PLAYER_TARGET_CHANGED" then
|
||||
SelectTarget()
|
||||
elseif targetKey and event == "UNIT_COMBAT" and arg1 == "target" then
|
||||
if arg2 == "HEAL" then return end
|
||||
|
||||
local amount = tonumber(arg4) or 0
|
||||
if amount > 0 then
|
||||
damage = damage + amount
|
||||
end
|
||||
elseif targetKey and event == "UNIT_HEALTH" and arg1 == "target" then
|
||||
local currentPercent = _G.UnitHealth("target") or 0
|
||||
local diff = percent - currentPercent
|
||||
|
||||
if damage > 0 and diff > 0 then
|
||||
local estimate = ceil(damage / diff * 100)
|
||||
local data = mobdb[targetKey]
|
||||
|
||||
if not data or not data[2] or diff > data[2] then
|
||||
mobdb[targetKey] = {
|
||||
estimate,
|
||||
diff,
|
||||
data and data[3] and data[3] + 1 or 1,
|
||||
}
|
||||
elseif data then
|
||||
data[3] = data[3] and data[3] + 1 or 1
|
||||
end
|
||||
end
|
||||
|
||||
damage = 0
|
||||
percent = currentPercent
|
||||
end
|
||||
end)
|
||||
|
||||
ShaguTweaks.libhealth = libhealth
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user