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