349 lines
12 KiB
Lua
349 lines
12 KiB
Lua
BNP = BNP or {}
|
|
|
|
-- Tank Mode for SuperWoW GUID nameplates.
|
|
-- Default: GREEN = hostile NPC currently targets the player, RED = another unit.
|
|
-- Normal Tank Mode is fixed: GREEN = aggro, RED = no aggro.
|
|
-- Invert Tank Colors uses two independently user-selectable colors.
|
|
-- Optional third color for in-combat NPCs temporarily unable to attack.
|
|
-- The existing two-color behavior is preserved while this option is disabled.
|
|
|
|
local GREEN_R, GREEN_G, GREEN_B = 0.00, 1.00, 0.00
|
|
local RED_R, RED_G, RED_B = 1.00, 0.00, 0.00
|
|
|
|
-- Only effects that prevent attacking; roots, snares, silence and disarm
|
|
-- alone do not qualify. Shared with the existing aura snapshot reader.
|
|
BNP.TankDisablingCC = {
|
|
fear=true, howl_of_terror=true, banish=true, death_coil=true,
|
|
freezing_trap_effect=true, scatter_shot=true, wyvern_sting=true,
|
|
intimidation=true, scare_beast=true, improved_concussive_shot=true,
|
|
polymorph=true, impact=true, psychic_scream=true, blackout=true,
|
|
shackle_undead=true, hammer_of_justice=true, repentance=true, turn_undead=true,
|
|
gouge=true, kidney_shot=true, cheap_shot=true, sap=true, blind=true,
|
|
bash=true, pounce=true, hibernate=true, intimidating_shout=true,
|
|
charge_stun=true, intercept_stun=true, concussion_blow=true, revenge_stun=true,
|
|
}
|
|
|
|
local function HasFlag(flags, mask)
|
|
-- Lua 5.0 compatible, without requiring a bit library.
|
|
return math.mod(math.floor(flags / mask), 2) == 1
|
|
end
|
|
|
|
local function HasCachedControl(plate, guid, now)
|
|
-- Authoritative UNIT_AURA snapshot, independent of CC icon visibility.
|
|
if plate.BNPClassicAuraEventDriven and plate.BNPTankControlGUID ~= guid
|
|
and BNP.PrimeTankControlSnapshot then
|
|
BNP:PrimeTankControlSnapshot(plate, guid, now)
|
|
end
|
|
if plate.BNPTankControlGUID == guid and plate.BNPClassicAuraEventDriven then
|
|
return plate.BNPTankControlled and true or false
|
|
end
|
|
local cache = BNP.guidAuras and BNP.guidAuras[guid]
|
|
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
|
|
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
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function IsTemporarilyInactive(plate, guid, now, noTarget)
|
|
-- One optional descriptor read covers combat, stun, confusion and fleeing,
|
|
-- including low-health fleeing with a retained target. No tooltip/aura scan.
|
|
local flags
|
|
if GetUnitField then
|
|
local ok, value = pcall(GetUnitField, guid, "flags")
|
|
if ok and type(value) == "number" then flags = value end
|
|
end
|
|
local inCombat
|
|
if flags then inCombat = HasFlag(flags, 524288)
|
|
else inCombat = UnitAffectingCombat and UnitAffectingCombat(guid) end
|
|
if not inCombat then return false end
|
|
if flags and (HasFlag(flags, 262144) or HasFlag(flags, 4194304) or HasFlag(flags, 8388608)) then
|
|
return true
|
|
end
|
|
return HasCachedControl(plate, guid, now) or noTarget
|
|
end
|
|
|
|
local playerGUID = nil
|
|
|
|
local function GetPlayerGUID()
|
|
local exists, guid = UnitExists("player")
|
|
if exists and guid then playerGUID = guid end
|
|
return playerGUID
|
|
end
|
|
|
|
local function GetPlateGUID(plate)
|
|
if not plate or not plate.GetName then return nil end
|
|
-- The ClassicAPI token is stable across camera projection changes. Only
|
|
-- use it while the API maps it back to this exact frame.
|
|
local token = plate.BNPClassicUnitToken
|
|
if token and BNP._ClassicUnitGUID and BNP._ClassicGetNamePlateForUnit
|
|
and BNP._ClassicGetNamePlateForUnit(token) == plate then
|
|
local stableGUID = BNP._ClassicUnitGUID(token)
|
|
if stableGUID then return stableGUID end
|
|
end
|
|
local guid = plate:GetName(1)
|
|
if guid and guid ~= "" then return guid end
|
|
return nil
|
|
end
|
|
|
|
local function GetTargetGUID(guid)
|
|
if not guid then return nil end
|
|
local exists, targetGUID = UnitExists(guid .. "target")
|
|
if exists then return targetGUID, false end
|
|
return nil, true
|
|
end
|
|
|
|
local function GetBar(plate)
|
|
return plate and (plate.healthbar or plate.healthBar) or nil
|
|
end
|
|
|
|
local function IsHostileNPC(guid)
|
|
if not guid or not UnitExists(guid) then return false end
|
|
if UnitIsPlayer and UnitIsPlayer(guid) then return false end
|
|
-- Player-controlled pets/guardians are not NPC tank targets. Treating them
|
|
-- as hostile NPCs made Tank Mode overwrite their normal pet plate colors.
|
|
if UnitPlayerControlled and UnitPlayerControlled(guid) then return false end
|
|
return UnitCanAttack("player", guid) and true or false
|
|
end
|
|
|
|
local function IsForeignTaggedGUID(guid)
|
|
if not guid or not UnitExists(guid) then return false end
|
|
if UnitCanAttack and not UnitCanAttack("player", guid) then return false end
|
|
if not UnitIsTapped or not UnitIsTapped(guid) then return false end
|
|
if UnitIsTappedByPlayer and UnitIsTappedByPlayer(guid) then return false end
|
|
return true
|
|
end
|
|
|
|
local function RestoreNormalColor(plate, bar)
|
|
-- Do not restore a cached RGB value. Outside an active Tank Mode state,
|
|
-- Blizzard owns the NPC healthbar color completely.
|
|
-- Only undo the new third color for this same live NPC. Never replay a
|
|
-- cached reaction color onto a recycled, player-controlled or tagged plate.
|
|
local guid = plate.BNPTankNoTargetActive and bar and GetPlateGUID(plate)
|
|
if guid and guid == plate.BNPTankUnitGUID
|
|
and IsHostileNPC(guid) and not IsForeignTaggedGUID(guid) then
|
|
local reaction = UnitReaction and UnitReaction(guid, "player")
|
|
if reaction then
|
|
if reaction > 4 then bar:SetStatusBarColor(0, 1, 0, 1)
|
|
elseif reaction == 4 then bar:SetStatusBarColor(1, 1, 0, 1)
|
|
else bar:SetStatusBarColor(1, 0, 0, 1) end
|
|
end
|
|
end
|
|
plate.BNPTankNoTargetActive = nil
|
|
plate.BNPTankNoTargetSince = nil
|
|
plate.BNPTankUnitGUID = nil
|
|
plate.BNPTankColorGUID = nil
|
|
plate.BNPTankActive = nil
|
|
plate.BNPTankR = nil
|
|
plate.BNPTankG = nil
|
|
plate.BNPTankB = nil
|
|
end
|
|
|
|
local function ClearTankState(plate)
|
|
if not plate then return end
|
|
plate.BNPTankNoTargetActive = nil
|
|
plate.BNPTankNoTargetSince = nil
|
|
plate.BNPTankUnitGUID = nil
|
|
plate.BNPTankColorGUID = nil
|
|
plate.BNPTankActive = nil
|
|
plate.BNPTankR = nil
|
|
plate.BNPTankG = nil
|
|
plate.BNPTankB = nil
|
|
end
|
|
|
|
local function ApplyTankColor(plate, bar, r, g, b)
|
|
bar:SetStatusBarColor(r, g, b, 1)
|
|
plate.BNPTankColorGUID = GetPlateGUID(plate)
|
|
plate.BNPTankActive = true
|
|
plate.BNPTankR = r
|
|
plate.BNPTankG = g
|
|
plate.BNPTankB = b
|
|
end
|
|
|
|
local function EnforceTankColor(plate)
|
|
if not plate or not plate.BNPTankActive then return end
|
|
-- Never paint a previous unit's cached color onto a recycled plate.
|
|
if not BNP:IsTankModeEnabled() or plate.BNPTankColorGUID ~= GetPlateGUID(plate) then
|
|
ClearTankState(plate)
|
|
return
|
|
end
|
|
local bar = GetBar(plate)
|
|
if not bar or not plate.BNPTankR then return end
|
|
|
|
local r, g, b = bar:GetStatusBarColor()
|
|
if math.abs(r - plate.BNPTankR) > 0.01
|
|
or math.abs(g - plate.BNPTankG) > 0.01
|
|
or math.abs(b - plate.BNPTankB) > 0.01 then
|
|
bar:SetStatusBarColor(plate.BNPTankR, plate.BNPTankG, plate.BNPTankB, 1)
|
|
end
|
|
end
|
|
|
|
function BNP:UpdateTankModePlate(plate)
|
|
if not plate or not plate:IsShown() then return end
|
|
|
|
local bar = GetBar(plate)
|
|
if not bar then return end
|
|
|
|
if not self:IsTankModeEnabled() then
|
|
RestoreNormalColor(plate, bar)
|
|
return
|
|
end
|
|
|
|
local guid = GetPlateGUID(plate)
|
|
if not IsHostileNPC(guid) then
|
|
-- NPC reaction colors belong to Blizzard. Never restore a stale cached
|
|
-- Tank Mode color onto neutral/friendly/recycled nameplates.
|
|
ClearTankState(plate)
|
|
return
|
|
end
|
|
|
|
-- Foreign-tag grey has priority over Tank Mode. Previously Tank Mode wrote
|
|
-- RED every 0.10s while fullalpha.lua restored GREY every frame, producing
|
|
-- the visible grey/red flicker. Clear any cached tank color and let the
|
|
-- foreign-tag renderer own the bar until the mob is ours/untapped again.
|
|
if IsForeignTaggedGUID(guid) then
|
|
ClearTankState(plate)
|
|
return
|
|
end
|
|
|
|
local thirdColor = self:IsTankNoTargetEnabled()
|
|
if thirdColor then
|
|
if plate.BNPTankUnitGUID ~= guid then ClearTankState(plate) end
|
|
plate.BNPTankUnitGUID = guid
|
|
if UnitIsDead and UnitIsDead(guid) then
|
|
RestoreNormalColor(plate, bar)
|
|
return
|
|
end
|
|
elseif plate.BNPTankNoTargetSince or plate.BNPTankNoTargetActive then
|
|
RestoreNormalColor(plate, bar)
|
|
end
|
|
|
|
local myGUID = GetPlayerGUID()
|
|
if not myGUID then return end
|
|
|
|
local targetGUID, noTarget = GetTargetGUID(guid)
|
|
local now = thirdColor and GetTime()
|
|
if thirdColor and IsTemporarilyInactive(plate, guid, now, noTarget) then
|
|
local r, g, b = self:GetTankNoTargetColor()
|
|
ApplyTankColor(plate, bar, r, g, b)
|
|
plate.BNPTankNoTargetActive = true
|
|
return
|
|
end
|
|
plate.BNPTankNoTargetSince = nil
|
|
if thirdColor and not targetGUID then
|
|
-- No active control and no readable target: relinquish the third color.
|
|
RestoreNormalColor(plate, bar)
|
|
return
|
|
end
|
|
plate.BNPTankNoTargetActive = nil
|
|
local inverted = self:AreTankModeColorsInverted()
|
|
|
|
if targetGUID and targetGUID == myGUID then
|
|
if inverted then
|
|
local r, g, b = self:GetInvertTankAggroColor()
|
|
ApplyTankColor(plate, bar, r, g, b)
|
|
else
|
|
ApplyTankColor(plate, bar, GREEN_R, GREEN_G, GREEN_B)
|
|
end
|
|
elseif targetGUID then
|
|
if inverted then
|
|
local r, g, b = self:GetInvertTankNoAggroColor()
|
|
ApplyTankColor(plate, bar, r, g, b)
|
|
else
|
|
ApplyTankColor(plate, bar, RED_R, RED_G, RED_B)
|
|
end
|
|
else
|
|
RestoreNormalColor(plate, bar)
|
|
end
|
|
end
|
|
|
|
function BNP:RefreshTankMode()
|
|
local plate
|
|
for plate in pairs(self.plates or {}) do
|
|
if plate and plate:IsShown() then
|
|
self:UpdateTankModePlate(plate)
|
|
end
|
|
end
|
|
end
|
|
|
|
function BNP:UpdateTankMode()
|
|
local enabled = self:IsTankModeEnabled() and self:IsTankNoTargetEnabled()
|
|
if not enabled then
|
|
local plate
|
|
for plate in pairs(self.plates or {}) do
|
|
plate.BNPTankControlGUID = nil
|
|
plate.BNPTankControlled = nil
|
|
end
|
|
end
|
|
self:RefreshTankMode()
|
|
end
|
|
|
|
function BNP:InstallTankMode()
|
|
if self.BNPTankModeInstalled then return end
|
|
if not self.libnameplate or not self.libnameplate.OnUpdate then return end
|
|
|
|
GetPlayerGUID()
|
|
|
|
table.insert(self.libnameplate.OnInit, function(plate)
|
|
local current = plate or this
|
|
if not current then return end
|
|
|
|
ClearTankState(current)
|
|
|
|
local bar = GetBar(current)
|
|
if bar and not current.BNPTankColorHooked then
|
|
local old = bar:GetScript("OnValueChanged")
|
|
bar:SetScript("OnValueChanged", function()
|
|
if old then old() end
|
|
EnforceTankColor(current)
|
|
end)
|
|
-- Blizzard may repaint on camera movement without changing HP. Keep
|
|
-- only the cached color in sync here; combat/aura reads stay throttled.
|
|
local oldUpdate = bar:GetScript("OnUpdate")
|
|
bar:SetScript("OnUpdate", function()
|
|
if oldUpdate then oldUpdate() end
|
|
EnforceTankColor(current)
|
|
end)
|
|
current.BNPTankColorHooked = true
|
|
end
|
|
end)
|
|
|
|
table.insert(self.libnameplate.OnShow, function(plate)
|
|
local current = plate or this
|
|
if not current then return end
|
|
ClearTankState(current)
|
|
current.BNPTankControlGUID = nil
|
|
current.BNPTankControlled = nil
|
|
BNP:UpdateTankModePlate(current)
|
|
end)
|
|
|
|
table.insert(self.libnameplate.OnUpdate, function(plate, elapsed)
|
|
local current = plate or this
|
|
if not current then return end
|
|
|
|
-- Tank Mode is optional. Avoid all GUID/target/tap queries while it is off.
|
|
-- Only clean an old BNP-owned state if one is still present.
|
|
if not BNP:IsTankModeEnabled() then
|
|
if current.BNPTankActive or current.BNPTankNoTargetSince then
|
|
RestoreNormalColor(current, GetBar(current))
|
|
end
|
|
return
|
|
end
|
|
|
|
BNP:UpdateTankModePlate(current)
|
|
EnforceTankColor(current)
|
|
end)
|
|
|
|
self.BNPTankModeInstalled = true
|
|
end
|