diff --git a/core/options.lua b/core/options.lua index 3345e1a..9a5baed 100644 --- a/core/options.lua +++ b/core/options.lua @@ -1465,7 +1465,7 @@ function BNP:CreateOptions() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("Inactive Combat Color", 1, 0.82, 0) GameTooltip:AddLine("Uses a third color only for enemies in combat that are temporarily unable to attack (fear, sheep, stuns or fleeing). Idle enemies are excluded.", 1, 1, 1, true) - GameTooltip:AddLine("Requires a confirmed CC or fleeing state, even if the enemy still has a target. Losing a target alone does not change the color. Independent of Invert Tank Colors.", 0.8, 0.8, 0.8, true) + GameTooltip:AddLine("Detects active CC and fleeing states. Also treats living enemies in combat at 30% health or less with no target as likely fleeing. Idle enemies and corpses are excluded. Independent of Invert Tank Colors.", 0.8, 0.8, 0.8, true) GameTooltip:Show() end) noTarget:SetScript("OnLeave", function() GameTooltip:Hide() end) diff --git a/modules/auras.lua b/modules/auras.lua index 87b1486..1014411 100644 --- a/modules/auras.lua +++ b/modules/auras.lua @@ -2340,7 +2340,7 @@ local function CaptureLiveDebuffSnapshot(unit, now) local auraName, texture, stacks, dtype, duration, expirationTime, source, isStealable, nameplateShowPersonal, auraSpellID = BNP._ClassicUnitAuraBySlot(unit, slots[i]) - if auraName then + if auraName or (auraSpellID and auraSpellID > 0) then count = count + 1 local entry = entries[count] if not entry then @@ -2937,20 +2937,24 @@ function BNP:CacheTankControlSnapshot(plate, guid, snapshot) or not self:IsTankModeEnabled() then plate.BNPTankControlGUID = nil plate.BNPTankControlled = nil + plate.BNPTankChargeStun = nil return end - local controlled = false + local controlled, chargeStun = false, false local i for i = 1, snapshot.count do local entry = snapshot.entries[i] local def = ResolveGlobalCCDef(entry[4], entry[10]) if def and self.TankDisablingCC and self.TankDisablingCC[def.key] then controlled = true - break + -- The applied Charge stun can arrive before the combat flag. It is + -- itself evidence of an opening attack; a cast attempt alone is not. + if def.key == "charge_stun" then chargeStun = true; break end end end plate.BNPTankControlGUID = guid plate.BNPTankControlled = controlled + plate.BNPTankChargeStun = chargeStun end -- One initial read when enabling the option on an already visible plate. diff --git a/modules/fullalpha.lua b/modules/fullalpha.lua index 7554a98..014c8c0 100644 --- a/modules/fullalpha.lua +++ b/modules/fullalpha.lua @@ -967,14 +967,16 @@ local function InstallAlphaGuard(plate) -- query inside ApplyForeignTagVisual is already cached to 10 Hz. ApplyForeignTagVisual(current) + -- Native projection can reset the raid icon anchor on any frame. Correct + -- visible marks before rendering; the Y-offset loop leaves their anchors + -- to raidmarks.lua. This only compares existing geometry, with no unit or + -- aura queries, and skips plates whose native raid icon is hidden. + if BNP.MaintainRaidMarkPosition then BNP:MaintainRaidMarkPosition(current) end + current.BNPVisualGuardElapsed = (current.BNPVisualGuardElapsed or 0) + (arg1 or 0) if current.BNPVisualGuardElapsed >= 0.05 then current.BNPVisualGuardElapsed = 0 - -- Blizzard and the scale/Y-offset wrapper may restore the native raid icon - -- anchor while the plate is being projected. Reapply the optional custom - -- Raid Mark position on the throttled safety pass. - if BNP.MaintainRaidMarkPosition then BNP:MaintainRaidMarkPosition(current) end if BNP.MaintainTargetScale then BNP:MaintainTargetScale(current) end ApplyTargetGlow(current) ApplyTargetArrows(current) diff --git a/modules/raidmarks.lua b/modules/raidmarks.lua index d9ba00f..df03d3a 100644 --- a/modules/raidmarks.lua +++ b/modules/raidmarks.lua @@ -47,6 +47,13 @@ function BNP:ApplyRaidMarkPosition(plate) local point, relativeTo, relativePoint, x, y = GetCustomAnchor(plate, position) if not point or not relativeTo then return end + -- Keep the native icon with the scaled plate visuals if the client restores + -- its parent. This preserves the same scaling and alpha as the healthbar. + local wrapper = plate.BNPScaleWrapper + if wrapper and icon.GetParent and icon.SetParent and icon:GetParent() ~= wrapper then + icon:SetParent(wrapper) + end + if not PointMatches(icon, point, relativeTo, relativePoint, x, y) then if not icon.ClearAllPoints or not icon.SetPoint then return end icon:ClearAllPoints() @@ -60,7 +67,9 @@ function BNP:MaintainRaidMarkPosition(plate) if not plate or not plate.IsShown or not plate:IsShown() then return end local icon = plate.raidicon if not icon then return end - -- This path runs from the per-frame plate guard. Most plates have no active + -- This path runs after native/offset updates on EVERY rendered frame. A + -- throttled repair leaves the native anchor visible between repairs. + -- Most plates have no active -- raid mark, so avoid GetPoint/anchor comparisons until Blizzard actually -- shows the icon. The first visible frame will still be corrected instantly. if icon.IsShown and not icon:IsShown() then return end diff --git a/modules/scale.lua b/modules/scale.lua index f2ab8e9..e465530 100644 --- a/modules/scale.lua +++ b/modules/scale.lua @@ -604,7 +604,13 @@ function BNP:MaintainNameplateYOffset(plate) local states = wrapper.BNPAnchorStates or {} local i for i = 1, table.getn(states) do - RestoreAnchorState(states[i], wrapper) + local state = states[i] + -- Raid marks own their custom Top/Left/Right anchor. Restoring the native + -- anchor here every frame fights raidmarks.lua and makes the icon jump + -- between both positions whenever the nameplate Y offset is enabled. + if state.object ~= plate.raidicon or not self.MaintainRaidMarkPosition then + RestoreAnchorState(state, wrapper) + end end ApplyWrapperOffset(plate, wrapper) diff --git a/modules/tankmode.lua b/modules/tankmode.lua index 41fa218..e6f30fc 100644 --- a/modules/tankmode.lua +++ b/modules/tankmode.lua @@ -35,27 +35,32 @@ local function HasCachedControl(plate, guid, now) BNP:PrimeTankControlSnapshot(plate, guid, now) end if plate.BNPTankControlGUID == guid and plate.BNPClassicAuraEventDriven then - return plate.BNPTankControlled and true or false + return plate.BNPTankControlled and true or false, plate.BNPTankChargeStun and true or false end local cache = BNP.guidAuras and BNP.guidAuras[guid] + local controlled = false local key, aura if cache then for key, aura in pairs(cache) do - if BNP.TankDisablingCC[key] and aura.expires and aura.expires > now then return true end + if BNP.TankDisablingCC[key] and aura.expires and aura.expires > now then + controlled = true + if key == "charge_stun" then return true, true end + end end end cache = BNP.guidLiveCCs and BNP.guidLiveCCs[guid] if cache then for key, aura in pairs(cache) do if BNP.TankDisablingCC[key] and (aura.eventDriven or now - (aura.lastSeen or 0) <= 0.60) then - return true + controlled = true + if key == "charge_stun" and aura.expires and aura.expires > now then return true, true end end end end - return false + return controlled, false end -local function IsTemporarilyInactive(plate, guid, now) +local function IsTemporarilyInactive(plate, guid, now, targetExists) -- One optional descriptor read covers combat, stun, confusion and fleeing, -- including low-health fleeing with a retained target. No tooltip/aura scan. local flags @@ -66,13 +71,29 @@ local function IsTemporarilyInactive(plate, guid, now) local inCombat if flags then inCombat = HasFlag(flags, 524288) else inCombat = UnitAffectingCombat and UnitAffectingCombat(guid) end - if not inCombat then return false end + if not inCombat then + -- Do not wait for the combat flag when the actual opening Charge stun + -- is already in the harmful-aura snapshot. Pre-pull sheep/sap stay out. + local controlled, chargeStun = HasCachedControl(plate, guid, now) + return controlled and chargeStun + end if flags and (HasFlag(flags, 262144) or HasFlag(flags, 4194304) or HasFlag(flags, 8388608)) then return true end - -- A missing target alone is not evidence of CC or fleeing. It also occurs - -- during death, pet return and ordinary target switches. - return HasCachedControl(plate, guid, now) + if HasCachedControl(plate, guid, now) then return true end + + -- Fleeing to seek assistance can clear the target without a CC aura or a + -- fleeing flag, and GetUnitField is optional (Nampower). Keep this fallback + -- deliberately narrow: in combat, no target, alive, <= 30% HP, not casting. + -- This is a heuristic, not proof of fleeing. No per-frame scan or timer. + if targetExists or not UnitHealth or not UnitHealthMax then return false end + local health = UnitHealth(guid) + if not health or health <= 0 then return false end + local maximum = UnitHealthMax(guid) + if not maximum or maximum <= 0 or health / maximum > 0.30 then return false end + if UnitCastingInfo and UnitCastingInfo(guid) then return false end + if UnitChannelInfo and UnitChannelInfo(guid) then return false end + return true end local playerGUID = nil @@ -99,10 +120,11 @@ local function GetPlateGUID(plate) end local function GetTargetGUID(guid) - if not guid then return nil end + if not guid then return nil, false end local exists, targetGUID = UnitExists(guid .. "target") - if exists then return targetGUID end - return nil + -- An existing target whose GUID is temporarily unavailable is not "none". + if exists then return targetGUID, true end + return nil, false end local function GetBar(plate) @@ -246,9 +268,9 @@ function BNP:UpdateTankModePlate(plate) local myGUID = GetPlayerGUID() if not myGUID then return end - local targetGUID = GetTargetGUID(guid) + local targetGUID, targetExists = GetTargetGUID(guid) local now = thirdColor and GetTime() - if thirdColor and IsTemporarilyInactive(plate, guid, now) then + if thirdColor and IsTemporarilyInactive(plate, guid, now, targetExists) then local r, g, b = self:GetTankNoTargetColor() ApplyTankColor(plate, bar, r, g, b) plate.BNPTankNoTargetActive = true @@ -298,6 +320,7 @@ function BNP:UpdateTankMode() for plate in pairs(self.plates or {}) do plate.BNPTankControlGUID = nil plate.BNPTankControlled = nil + plate.BNPTankChargeStun = nil end end self:RefreshTankMode() @@ -339,6 +362,7 @@ function BNP:InstallTankMode() ClearTankState(current) current.BNPTankControlGUID = nil current.BNPTankControlled = nil + current.BNPTankChargeStun = nil BNP:UpdateTankModePlate(current) end) diff --git a/spells/warrior.lua b/spells/warrior.lua index 93f613b..d0ad071 100644 --- a/spells/warrior.lua +++ b/spells/warrior.lua @@ -78,6 +78,9 @@ BNP.WarriorAuras = { { key = "charge_stun", names = { "Charge Stun" }, + -- The harmful stun, not the Charge cast (100 / 6178 / 11578). + -- Match even before a localized aura name is available. + spellIDs = { 7922 }, duration = 1, }, {