576 lines
18 KiB
Lua
576 lines
18 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 GetTargetScaleMultiplier(plate)
|
|
if plate and plate.BNPIsCurrentTarget and BNP.IsTargetScaleEnabled and BNP:IsTargetScaleEnabled() then
|
|
if BNP.GetTargetScale then return BNP:GetTargetScale() end
|
|
end
|
|
return 1.0
|
|
end
|
|
|
|
local function EffectiveScale(plate)
|
|
local uiScale = 1.0
|
|
if UIParent and UIParent.GetScale then
|
|
uiScale = UIParent:GetScale() or 1.0
|
|
end
|
|
return uiScale * BNP:GetNameplateScale() * GetTargetScaleMultiplier(plate)
|
|
end
|
|
|
|
local function ApplyDetachedTargetScale(plate, multiplier)
|
|
if not plate then return end
|
|
multiplier = multiplier or 1.0
|
|
|
|
-- Auras / CC / immunities intentionally live beside the projected nameplate
|
|
-- so they do not inherit Blizzard alpha changes. The totem replacement icon
|
|
-- is also a direct child of the projected plate rather than BNPScaleWrapper.
|
|
-- Scale these few detached visuals explicitly so Target Scale enlarges the
|
|
-- COMPLETE target presentation, not only the healthbar/border/name.
|
|
local function ApplyObject(object)
|
|
if object and object.SetScale and object.BNPTargetScaleApplied ~= multiplier then
|
|
object:SetScale(multiplier)
|
|
object.BNPTargetScaleApplied = multiplier
|
|
end
|
|
end
|
|
|
|
-- Do not collect these in an ipairs table: most plates do not have every
|
|
-- optional container, and Lua 5.1 stops ipairs at the first nil entry.
|
|
ApplyObject(plate.BNPAuraContainer)
|
|
ApplyObject(plate.BNPCCContainer)
|
|
ApplyObject(plate.BNPImmunityContainer)
|
|
ApplyObject(plate.BNPTotemIndicator)
|
|
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 then return nil end
|
|
|
|
-- Prefer ClassicAPI's exact nameplateN token when it is available. Besides
|
|
-- being stable for the lifetime of the projected plate, this is a normal
|
|
-- unit token and gives GameTooltip the same reaction/hostility information
|
|
-- it receives from Blizzard unit frames.
|
|
local classicToken = plate.BNPClassicUnitToken
|
|
if classicToken and UnitExists then
|
|
local existsOK, exists = pcall(UnitExists, classicToken)
|
|
if existsOK and exists then return classicToken end
|
|
end
|
|
|
|
-- SuperWoW fallback: frame:GetName(1) returns the GUID attached to the
|
|
-- nameplate. SuperWoW extends unit-taking functions to accept that GUID.
|
|
if not plate.GetName then return nil end
|
|
local ok, unit = pcall(function() return plate:GetName(1) end)
|
|
if not ok or not unit or 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
|
|
|
|
if GameTooltip_SetDefaultAnchor then
|
|
GameTooltip_SetDefaultAnchor(GameTooltip, wrapper)
|
|
elseif GameTooltip.SetOwner then
|
|
GameTooltip:SetOwner(wrapper, "ANCHOR_CURSOR")
|
|
end
|
|
|
|
-- Do NOT build the tooltip from the synthetic "mouseover" token.
|
|
-- SetMouseoverUnit() is still required for mouseover macros/addons, but on
|
|
-- some SuperWoW/ClassicAPI combinations the synthetic token can briefly
|
|
-- expose stale reaction data. That is visible as a hostile NPC name
|
|
-- flickering between yellow and red in GameTooltip. Feed GameTooltip the
|
|
-- exact plate unit instead (nameplateN when possible, GUID otherwise), just
|
|
-- like established custom unit-frame implementations do.
|
|
local ok = pcall(function() GameTooltip:SetUnit(unit) end)
|
|
if not ok then
|
|
-- Compatibility fallback for older clients that cannot tooltip a GUID.
|
|
pcall(function() GameTooltip:SetUnit("mouseover") 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 targetMultiplier = GetTargetScaleMultiplier(plate)
|
|
local scale = EffectiveScale(plate)
|
|
|
|
-- Target identity is checked every frame, but only touch frame geometry when
|
|
-- the actual scale changes. This keeps target switching instant without
|
|
-- continuously rewriting width/height on every visible nameplate.
|
|
if wrapper.BNPLastAppliedScale ~= scale then
|
|
wrapper:SetScale(scale)
|
|
|
|
-- 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)
|
|
wrapper.BNPLastAppliedScale = scale
|
|
end
|
|
|
|
wrapper.BNPLastTargetScaleMultiplier = targetMultiplier
|
|
ApplyDetachedTargetScale(plate, targetMultiplier)
|
|
ApplyWrapperOffset(plate, wrapper)
|
|
end
|
|
|
|
function BNP:MaintainTargetScale(plate)
|
|
if not plate or not plate.IsShown or not plate:IsShown() then return end
|
|
EnsureScaleWrapper(plate)
|
|
|
|
local wrapper = plate.BNPScaleWrapper
|
|
if not wrapper then return end
|
|
|
|
local targetMultiplier = GetTargetScaleMultiplier(plate)
|
|
if wrapper.BNPLastTargetScaleMultiplier ~= targetMultiplier then
|
|
self:ApplyNameplateScale(plate)
|
|
return
|
|
end
|
|
|
|
-- Detached frames can be created after a unit was already targeted. Keep
|
|
-- their cached multiplier in sync without doing any wrapper geometry work.
|
|
ApplyDetachedTargetScale(plate, targetMultiplier)
|
|
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:RefreshTargetScale()
|
|
self:ApplyNameplateScaleAll()
|
|
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
|
|
|
|
-- Blizzard can reset projected nameplate geometry while a plate is hidden.
|
|
-- When the same target reappears, the cached multiplier is still identical
|
|
-- (for example 1.20 -> 1.20), so MaintainTargetScale would previously
|
|
-- assume nothing changed and skip reapplying the target geometry. That left
|
|
-- a recycled/reappearing target visually broken. Invalidate only BNP's
|
|
-- scale caches on every OnShow so the current plate is rebuilt once from
|
|
-- the known original dimensions. The strict target resolver in fullalpha
|
|
-- immediately corrects the multiplier afterwards if the recycled frame now
|
|
-- belongs to a different unit.
|
|
local wrapper = current.BNPScaleWrapper
|
|
if wrapper then
|
|
wrapper.BNPLastAppliedScale = nil
|
|
wrapper.BNPLastTargetScaleMultiplier = nil
|
|
end
|
|
|
|
local function InvalidateDetached(object)
|
|
if object then object.BNPTargetScaleApplied = nil end
|
|
end
|
|
InvalidateDetached(current.BNPAuraContainer)
|
|
InvalidateDetached(current.BNPCCContainer)
|
|
InvalidateDetached(current.BNPImmunityContainer)
|
|
InvalidateDetached(current.BNPTotemIndicator)
|
|
|
|
BNP:ApplyNameplateScale(current)
|
|
end)
|
|
end
|