Files
BlizzNameplatesPlus/modules/scale.lua
T
2026-09-09 17:57:40 +02:00

469 lines
13 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 StateYOffset(state)
if state and state.BNPNameAnchor and BNP.GetNameFontYOffset then
return BNP:GetNameFontYOffset()
end
return 0
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 + StateYOffset(state))) > 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 + StateYOffset(state)
)
end
end
local function SetHoverGlow(plate, shown)
if not plate or not plate.glow then return end
if shown then
if plate.glow.Show then
plate.glow:Show()
end
else
if plate.glow.Hide then
plate.glow:Hide()
end
end
end
-- SuperWoW exposes the attached unit GUID through nameplate:GetName(1).
-- Its SetMouseoverUnit(unit) API is specifically intended for custom unit
-- frames: set it on OnEnter and clear it on OnLeave. BNP's scale/Y-offset
-- wrapper owns the mouse hit area, so without this bridge the client never
-- receives a real "mouseover" unit even though clicks are forwarded to the
-- original Blizzard nameplate.
local function GetPlateMouseoverUnit(plate)
if not plate or not plate.GetName then return nil end
local ok, unit = pcall(function() return plate:GetName(1) end)
if not ok or not unit then return nil end
if UnitExists then
local existsOK, exists = pcall(UnitExists, unit)
if existsOK and not exists then return nil end
end
return unit
end
local function ShowPlateTooltip(wrapper, unit)
if not wrapper or not unit or not GameTooltip or not GameTooltip.SetUnit then return end
-- SetMouseoverUnit() has already made "mouseover" a valid unit. Prefer
-- that token so tooltip addons see the same unit surface as mouseover macros.
if GameTooltip_SetDefaultAnchor then
GameTooltip_SetDefaultAnchor(GameTooltip, wrapper)
elseif GameTooltip.SetOwner then
GameTooltip:SetOwner(wrapper, "ANCHOR_CURSOR")
end
local ok = pcall(function() GameTooltip:SetUnit("mouseover") end)
if not ok then
pcall(function() GameTooltip:SetUnit(unit) end)
end
if GameTooltip.Show then GameTooltip:Show() end
end
local function SetWrapperMouseover(plate, wrapper)
local unit = GetPlateMouseoverUnit(plate)
if not unit then return end
-- Keep ownership so a late OnLeave from an overlapping/recycled plate cannot
-- clear the mouseover unit that a newer wrapper has just established.
BNP.BNPMouseoverOwner = wrapper
wrapper.BNPMouseoverUnit = unit
if SetMouseoverUnit then
local ok = pcall(SetMouseoverUnit, unit)
if ok then
ShowPlateTooltip(wrapper, unit)
end
end
end
local function ClearWrapperMouseover(wrapper)
if BNP.BNPMouseoverOwner ~= wrapper then return end
BNP.BNPMouseoverOwner = nil
wrapper.BNPMouseoverUnit = nil
if SetMouseoverUnit then
pcall(SetMouseoverUnit)
end
if GameTooltip and GameTooltip.Hide then
GameTooltip:Hide()
end
end
local function SetYOffsetMouseMode(plate, wrapper, offset)
if not plate or not wrapper then return end
-- Always use the visual wrapper as BNP's interactive frame, including at
-- Y Offset 0. Previously offset 0 used Blizzard's native Button while
-- positive offsets used BNPScaleWrapper. That split mouse path caused the
-- original Blizzard mouseover glow to disappear at offset 0.
--
-- Keeping clicks, hit geometry and hover feedback on the same frame also
-- guarantees that the clickable area follows the visible plate at every
-- configured Y offset.
if plate.EnableMouse then
plate:EnableMouse(false)
end
if wrapper.EnableMouse then
wrapper:EnableMouse(true)
end
if wrapper.RegisterForClicks then
-- Preserve the classic Blizzard nameplate mouse behavior:
-- left click selects the unit, right click selects it and starts auto attack.
wrapper:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
if not wrapper.BNPClickForwardInstalled then
wrapper:SetScript("OnClick", function()
-- The wrapper exists only because BNP visually offsets/scales the original
-- Blizzard plate. Keep Blizzard's native nameplate click semantics by
-- forwarding the ACTUAL mouse button to the original Button.
--
-- parent:Click() without an argument always simulates LeftButton. That was
-- the reason right click turned into a plain target-select while BNP was
-- enabled. Do not call AttackTarget() manually here: it toggles auto attack
-- and is not equivalent to Blizzard's native nameplate right-click path.
local mouseButton = arg1 or "LeftButton"
local current = this or wrapper
local parent = current and current.plate or plate
if parent and parent:IsShown() and parent.Click then
parent:Click(mouseButton)
end
end)
wrapper.BNPClickForwardInstalled = true
end
if not wrapper.BNPMouseoverFeedbackInstalled then
wrapper:SetScript("OnEnter", function()
local current = this or wrapper
local parent = current and current.plate or plate
SetHoverGlow(parent, true)
SetWrapperMouseover(parent, current)
end)
wrapper:SetScript("OnLeave", function()
local current = this or wrapper
local parent = current and current.plate or plate
SetHoverGlow(parent, false)
ClearWrapperMouseover(current)
end)
-- A projected nameplate can disappear/recycle while the cursor is still
-- inside its old bounds. Clear SuperWoW's synthetic mouseover in that case
-- as well so "mouseover" can never remain stuck on a vanished unit.
wrapper:SetScript("OnHide", function()
local current = this or wrapper
ClearWrapperMouseover(current)
end)
wrapper.BNPMouseoverFeedbackInstalled = true
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)
SetYOffsetMouseMode(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("Button", nil, plate)
wrapper:SetAllPoints(plate)
wrapper.plate = plate
wrapper.originalWidth = originalWidth
wrapper.originalHeight = originalHeight
wrapper.BNPAnchorStates = {}
wrapper:EnableMouse(false)
plate.BNPScaleWrapper = wrapper
-- 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
if object == plate.name then
state.BNPNameAnchor = true
end
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:ApplyNameFontYOffset(plate)
if not plate then return end
EnsureScaleWrapper(plate)
local wrapper = plate.BNPScaleWrapper
if not wrapper then return end
local states = wrapper.BNPAnchorStates or {}
local i
for i = 1, table.getn(states) do
local state = states[i]
if state and state.BNPNameAnchor then
RestoreAnchorState(state, wrapper)
end
end
end
function BNP:ApplyNameFontYOffsetAll()
local plate
for plate in pairs(self.plates or {}) do
if plate then self:ApplyNameFontYOffset(plate) end
end
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