Restore original Blizzard healthbar layering to prevent soft/blurry nameplate visuals.
385 lines
11 KiB
Lua
385 lines
11 KiB
Lua
BNP = BNP or {}
|
|
|
|
local MIN_SCALE = 0.50
|
|
local MAX_SCALE = 1.50
|
|
local DEFAULT_SCALE = 1.00
|
|
|
|
local function Clamp(value)
|
|
if value < MIN_SCALE then return MIN_SCALE end
|
|
if value > MAX_SCALE then return MAX_SCALE end
|
|
return value
|
|
end
|
|
|
|
function BNP:GetNameplateScale()
|
|
BNP_DB = BNP_DB or {}
|
|
return Clamp(tonumber(BNP_DB.nameplateScale) or DEFAULT_SCALE)
|
|
end
|
|
|
|
local function EffectiveScale()
|
|
local uiScale = 1.0
|
|
if UIParent and UIParent.GetScale then
|
|
uiScale = UIParent:GetScale() or 1.0
|
|
end
|
|
return uiScale * BNP:GetNameplateScale()
|
|
end
|
|
|
|
local function CaptureAnchorState(object, plate, wrapper)
|
|
if not object or not object.GetPoint then return nil end
|
|
|
|
local state = {
|
|
object = object,
|
|
points = {},
|
|
}
|
|
|
|
local count = 1
|
|
if object.GetNumPoints then
|
|
count = object:GetNumPoints() or 1
|
|
if count < 1 then count = 1 end
|
|
end
|
|
|
|
local i
|
|
for i = 1, count do
|
|
local point, relativeTo, relativePoint, x, y = object:GetPoint(i)
|
|
if point then
|
|
if relativeTo == plate then
|
|
relativeTo = wrapper
|
|
end
|
|
|
|
state.points[table.getn(state.points) + 1] = {
|
|
point = point,
|
|
relativeTo = relativeTo or wrapper,
|
|
relativePoint = relativePoint or point,
|
|
x = x or 0,
|
|
y = y or 0,
|
|
}
|
|
end
|
|
end
|
|
|
|
return state
|
|
end
|
|
|
|
local function AnchorStateMatches(state)
|
|
if not state or not state.object or not state.object.GetPoint then return false end
|
|
|
|
local object = state.object
|
|
local expectedCount = table.getn(state.points)
|
|
local currentCount = expectedCount
|
|
|
|
if object.GetNumPoints then
|
|
currentCount = object:GetNumPoints() or 0
|
|
end
|
|
|
|
if currentCount ~= expectedCount then
|
|
return false
|
|
end
|
|
|
|
local i
|
|
for i = 1, expectedCount do
|
|
local expected = state.points[i]
|
|
local point, relativeTo, relativePoint, x, y = object:GetPoint(i)
|
|
|
|
if point ~= expected.point
|
|
or relativeTo ~= expected.relativeTo
|
|
or relativePoint ~= expected.relativePoint
|
|
or math.abs((x or 0) - expected.x) > 0.01
|
|
or math.abs((y or 0) - expected.y) > 0.01 then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function RestoreAnchorState(state, wrapper)
|
|
if not state or not state.object then return end
|
|
|
|
local object = state.object
|
|
|
|
if object.GetParent and object.SetParent and object:GetParent() ~= wrapper then
|
|
object:SetParent(wrapper)
|
|
end
|
|
|
|
-- Do nothing in the common case. Blizzard normally only rewrites a subset
|
|
-- of nameplate anchors while moving, most notably the level FontString.
|
|
if AnchorStateMatches(state) then return end
|
|
|
|
if not object.ClearAllPoints or not object.SetPoint then return end
|
|
|
|
object:ClearAllPoints()
|
|
|
|
local i
|
|
for i = 1, table.getn(state.points) do
|
|
local p = state.points[i]
|
|
object:SetPoint(
|
|
p.point,
|
|
p.relativeTo or wrapper,
|
|
p.relativePoint or p.point,
|
|
p.x,
|
|
p.y
|
|
)
|
|
end
|
|
end
|
|
|
|
local VALID_STRATA = {
|
|
BACKGROUND=true, LOW=true, MEDIUM=true, HIGH=true,
|
|
DIALOG=true, FULLSCREEN=true, FULLSCREEN_DIALOG=true, TOOLTIP=true,
|
|
}
|
|
|
|
local function RestoreOriginalHitRect(plate)
|
|
if not plate or not plate.SetHitRectInsets or not plate.BNPOriginalHitRect then return end
|
|
local base = plate.BNPOriginalHitRect
|
|
plate:SetHitRectInsets(base.left, base.right, base.top, base.bottom)
|
|
end
|
|
|
|
local function CaptureOriginalHitRect(plate)
|
|
if not plate or plate.BNPOriginalHitRect then return end
|
|
|
|
local left, right, top, bottom = 0, 0, 0, 0
|
|
if plate.GetHitRectInsets then
|
|
local ok, l, r, t, b = pcall(plate.GetHitRectInsets, plate)
|
|
if ok then
|
|
left = tonumber(l) or 0
|
|
right = tonumber(r) or 0
|
|
top = tonumber(t) or 0
|
|
bottom = tonumber(b) or 0
|
|
end
|
|
end
|
|
|
|
plate.BNPOriginalHitRect = {
|
|
left = left,
|
|
right = right,
|
|
top = top,
|
|
bottom = bottom,
|
|
}
|
|
end
|
|
|
|
local function EnsureYOffsetClickProxy(plate, wrapper)
|
|
if not plate or not wrapper then return nil end
|
|
if plate.BNPYOffsetClickProxy then return plate.BNPYOffsetClickProxy end
|
|
|
|
-- A child hit rectangle cannot reliably live far outside the projected
|
|
-- Blizzard nameplate. Use an independent WorldFrame button at the actual
|
|
-- visual position instead. It is mouse-only and draws nothing.
|
|
local proxy = CreateFrame("Button", nil, WorldFrame)
|
|
proxy.plate = plate
|
|
proxy:EnableMouse(true)
|
|
if proxy.RegisterForClicks then
|
|
proxy:RegisterForClicks("LeftButtonUp")
|
|
end
|
|
|
|
local strata = plate.GetFrameStrata and plate:GetFrameStrata() or nil
|
|
if not VALID_STRATA[strata] then strata = "BACKGROUND" end
|
|
proxy:SetFrameStrata(strata)
|
|
if proxy.SetFrameLevel and plate.GetFrameLevel then
|
|
proxy:SetFrameLevel((plate:GetFrameLevel() or 0) + 20)
|
|
end
|
|
|
|
proxy:ClearAllPoints()
|
|
proxy:SetPoint("TOPLEFT", wrapper, "TOPLEFT", 0, 0)
|
|
proxy:SetPoint("BOTTOMRIGHT", wrapper, "BOTTOMRIGHT", 0, 0)
|
|
|
|
proxy:SetScript("OnClick", function()
|
|
local currentPlate = this and this.plate or plate
|
|
if not currentPlate or not currentPlate:IsShown() then return end
|
|
if not TargetUnit or not currentPlate.GetName then return end
|
|
|
|
local token = currentPlate:GetName(1)
|
|
if token and UnitExists and UnitExists(token) then
|
|
TargetUnit(token)
|
|
end
|
|
end)
|
|
|
|
proxy:Hide()
|
|
plate.BNPYOffsetClickProxy = proxy
|
|
return proxy
|
|
end
|
|
|
|
local function ApplyYOffsetClickProxy(plate, wrapper, offset)
|
|
CaptureOriginalHitRect(plate)
|
|
RestoreOriginalHitRect(plate)
|
|
|
|
local proxy = EnsureYOffsetClickProxy(plate, wrapper)
|
|
if not proxy then return end
|
|
|
|
if offset and offset > 0 and plate:IsShown() then
|
|
local strata = plate.GetFrameStrata and plate:GetFrameStrata() or nil
|
|
if not VALID_STRATA[strata] then strata = "BACKGROUND" end
|
|
if proxy:GetFrameStrata() ~= strata then proxy:SetFrameStrata(strata) end
|
|
if proxy.SetFrameLevel and plate.GetFrameLevel then
|
|
proxy:SetFrameLevel((plate:GetFrameLevel() or 0) + 20)
|
|
end
|
|
proxy:Show()
|
|
else
|
|
proxy:Hide()
|
|
end
|
|
end
|
|
|
|
local function ApplyWrapperOffset(plate, wrapper)
|
|
if not plate or not wrapper then return end
|
|
|
|
local offset = 0
|
|
if BNP.GetNameplateYOffset then
|
|
offset = BNP:GetNameplateYOffset()
|
|
end
|
|
|
|
-- Move only the child visual container. The WorldFrame-projected Blizzard
|
|
-- nameplate itself keeps its original anchor and can continue to be
|
|
-- repositioned by the client every frame.
|
|
wrapper:ClearAllPoints()
|
|
wrapper:SetPoint("TOPLEFT", plate, "TOPLEFT", 0, offset)
|
|
wrapper:SetPoint("BOTTOMRIGHT", plate, "BOTTOMRIGHT", 0, offset)
|
|
|
|
ApplyYOffsetClickProxy(plate, wrapper, offset)
|
|
end
|
|
|
|
local function EnsureScaleWrapper(plate)
|
|
if not plate or plate.BNPScaleWrapper then return end
|
|
|
|
local originalWidth = plate:GetWidth()
|
|
local originalHeight = plate:GetHeight()
|
|
|
|
local wrapper = CreateFrame("Frame", nil, plate)
|
|
wrapper:SetAllPoints(plate)
|
|
wrapper.plate = plate
|
|
wrapper.originalWidth = originalWidth
|
|
wrapper.originalHeight = originalHeight
|
|
wrapper.BNPAnchorStates = {}
|
|
|
|
plate.BNPScaleWrapper = wrapper
|
|
|
|
-- The click proxy is parented to WorldFrame so it must be hidden explicitly
|
|
-- when Blizzard hides/recycles this projected nameplate.
|
|
if not plate.BNPYOffsetHideHook then
|
|
local oldOnHide = plate:GetScript("OnHide")
|
|
plate:SetScript("OnHide", function(self)
|
|
local current = self or this or plate
|
|
if oldOnHide then oldOnHide(current) end
|
|
if current and current.BNPYOffsetClickProxy then
|
|
current.BNPYOffsetClickProxy:Hide()
|
|
end
|
|
end)
|
|
plate.BNPYOffsetHideHook = true
|
|
end
|
|
|
|
-- Capture the ORIGINAL Blizzard anchors before changing parentage.
|
|
-- Those stored anchors become BNP's authoritative visual layout whenever
|
|
-- Y-offset is enabled.
|
|
if plate.healthbar then
|
|
local state = CaptureAnchorState(plate.healthbar, plate, wrapper)
|
|
if state then
|
|
table.insert(wrapper.BNPAnchorStates, state)
|
|
end
|
|
|
|
plate.healthbar:SetParent(wrapper)
|
|
RestoreAnchorState(state, wrapper)
|
|
plate.healthbar:SetFrameLevel(1)
|
|
end
|
|
|
|
local regions = { plate:GetRegions() }
|
|
local i, object
|
|
for i, object in pairs(regions) do
|
|
if object and object.SetParent then
|
|
local state = CaptureAnchorState(object, plate, wrapper)
|
|
if state then
|
|
table.insert(wrapper.BNPAnchorStates, state)
|
|
end
|
|
|
|
object:SetParent(wrapper)
|
|
RestoreAnchorState(state, wrapper)
|
|
end
|
|
end
|
|
|
|
ApplyWrapperOffset(plate, wrapper)
|
|
end
|
|
|
|
function BNP:ApplyNameplateScale(plate)
|
|
if not plate then return end
|
|
EnsureScaleWrapper(plate)
|
|
|
|
local wrapper = plate.BNPScaleWrapper
|
|
if not wrapper then return end
|
|
|
|
local scale = EffectiveScale()
|
|
wrapper:SetScale(scale)
|
|
ApplyWrapperOffset(plate, wrapper)
|
|
|
|
-- ShaguTweaks also adjusts the original frame bounds so Blizzard's world
|
|
-- positioning remains correct while the child visuals honor UI scale.
|
|
plate:SetWidth(wrapper.originalWidth * scale)
|
|
plate:SetHeight(wrapper.originalHeight * scale)
|
|
end
|
|
|
|
function BNP:ApplyNameplateScaleAll()
|
|
local plate
|
|
for plate in pairs(self.plates) do
|
|
self:ApplyNameplateScale(plate)
|
|
end
|
|
end
|
|
|
|
function BNP:ApplyNameplateYOffsetAll()
|
|
local plate
|
|
for plate in pairs(self.plates or {}) do
|
|
if plate then
|
|
self:ApplyNameplateScale(plate)
|
|
end
|
|
end
|
|
end
|
|
|
|
function BNP:MaintainNameplateYOffset(plate)
|
|
if not plate or not plate:IsShown() then return end
|
|
if not self.GetNameplateYOffset or self:GetNameplateYOffset() <= 0 then return end
|
|
|
|
EnsureScaleWrapper(plate)
|
|
|
|
local wrapper = plate.BNPScaleWrapper
|
|
if not wrapper then return end
|
|
|
|
-- Never rediscover regions through plate:GetRegions() here. Once BNP moves
|
|
-- Blizzard regions into the wrapper they are no longer guaranteed to be
|
|
-- returned by plate:GetRegions(). Keep and maintain the original region
|
|
-- objects captured during wrapper creation instead.
|
|
local states = wrapper.BNPAnchorStates or {}
|
|
local i
|
|
for i = 1, table.getn(states) do
|
|
RestoreAnchorState(states[i], wrapper)
|
|
end
|
|
|
|
ApplyWrapperOffset(plate, wrapper)
|
|
end
|
|
|
|
function BNP:SetNameplateScale(value)
|
|
value = tonumber(value)
|
|
if not value then
|
|
self:Print("Usage: /bnp scale 0.5-1.5")
|
|
return
|
|
end
|
|
|
|
value = Clamp(value)
|
|
BNP_DB = BNP_DB or {}
|
|
BNP_DB.nameplateScale = value
|
|
self:ApplyNameplateScaleAll()
|
|
|
|
self:Print("Nameplate scale multiplier set to " .. tostring(value) .. ".")
|
|
end
|
|
|
|
function BNP:ResetNameplateScale()
|
|
BNP_DB = BNP_DB or {}
|
|
BNP_DB.nameplateScale = DEFAULT_SCALE
|
|
self:ApplyNameplateScaleAll()
|
|
self:Print("Nameplate scale multiplier reset to 1.0 (Shagu/UI scale behavior).")
|
|
end
|
|
|
|
if BNP.libnameplate then
|
|
table.insert(BNP.libnameplate.OnInit, function(plate)
|
|
local current = plate or this
|
|
if not current then return end
|
|
BNP:ApplyNameplateScale(current)
|
|
end)
|
|
|
|
table.insert(BNP.libnameplate.OnShow, function(plate)
|
|
local current = plate or this
|
|
if not current then return end
|
|
BNP:ApplyNameplateScale(current)
|
|
end)
|
|
end
|