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 GetProjectedPlateUnit(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 or unit == "" then return nil end return unit end local function UnitGUIDSafe(unit) local guidFn = BNP._ClassicUnitGUID or UnitGUID if not guidFn or not unit then return nil end local ok, guid = pcall(guidFn, unit) if ok then return guid end return nil end local function FindExactClassicPlateToken(plate) if not plate or not UnitExists then return nil end -- Do not trust a cached nameplateN token blindly. Nameplate frames are -- recycled and NAME_PLATE_UNIT_REMOVED / ADDED can land around the same -- frame that the cursor enters a reused plate. Resolve the CURRENT mapping -- on demand. This work only happens on mouse enter, never in the background. local getPlate = BNP._ClassicGetNamePlateForUnit if not getPlate and C_NamePlate then getPlate = C_NamePlate.GetNamePlateForUnit end if getPlate then local projected = GetProjectedPlateUnit(plate) local projectedGUID = UnitGUIDSafe(projected) or projected local i for i = 1, 40 do local token = "nameplate" .. i local existsOK, exists = pcall(UnitExists, token) if existsOK and exists then local mapOK, mappedPlate = pcall(getPlate, token) if mapOK and mappedPlate == plate then local tokenGUID = UnitGUIDSafe(token) if not projectedGUID or not tokenGUID or tokenGUID == projectedGUID then plate.BNPClassicUnitToken = token if tokenGUID then plate.BNPClassicGUID = tokenGUID end return token end end end end end -- Older ClassicAPI fallback: a cached token is still usable, but verify its -- GUID against the unit currently attached to the SuperWoW nameplate first. local cached = plate.BNPClassicUnitToken if cached then local existsOK, exists = pcall(UnitExists, cached) if existsOK and exists then local projected = GetProjectedPlateUnit(plate) local projectedGUID = UnitGUIDSafe(projected) or projected local cachedGUID = UnitGUIDSafe(cached) if not projectedGUID or not cachedGUID or cachedGUID == projectedGUID then return cached end end -- Never carry a proven stale token into GameTooltip. plate.BNPClassicUnitToken = nil plate.BNPClassicGUID = nil end return nil end local function GetPlateMouseoverUnit(plate) if not plate then return nil end -- Prefer a freshly resolved ClassicAPI nameplateN token. It carries the -- cleanest reaction data for GameTooltip and avoids recycled-token races. local classicToken = FindExactClassicPlateToken(plate) if classicToken then return classicToken end -- SuperWoW fallback: frame:GetName(1) returns the GUID attached to the -- nameplate. SuperWoW extends unit-taking functions to accept that GUID. local unit = GetProjectedPlateUnit(plate) if 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 ForceTooltipNPCReactionColor(unit) if not unit or not UnitReaction then return end -- Leave player tooltip coloring alone (class/faction/PvP coloring belongs to -- Blizzard). The reported bug is NPC reaction color only. if UnitIsPlayer then local playerOK, isPlayer = pcall(UnitIsPlayer, unit) if playerOK and isPlayer then return end end local reactionOK, reaction = pcall(UnitReaction, unit, "player") if not reactionOK or not reaction then return end local r, g, b if FACTION_BAR_COLORS and FACTION_BAR_COLORS[reaction] then local c = FACTION_BAR_COLORS[reaction] r, g, b = c.r, c.g, c.b elseif reaction <= 3 then r, g, b = 1.0, 0.0, 0.0 elseif reaction == 4 then r, g, b = 1.0, 1.0, 0.0 else r, g, b = 0.0, 1.0, 0.0 end local title if getglobal then title = getglobal("GameTooltipTextLeft1") end if not title and _G then title = _G["GameTooltipTextLeft1"] end if title and title.SetTextColor then title:SetTextColor(r, g, b) end 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. Feed the -- tooltip the exact current plate unit and then explicitly apply the NPC -- reaction color. Some custom-client builds populate the correct unit/name -- but leave line 1 with the previous neutral/hostile color. 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 ForceTooltipNPCReactionColor(unit) 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