445 lines
16 KiB
Lua
445 lines
16 KiB
Lua
if not BNP.libnameplate then return end
|
|
|
|
-- Lightweight PvP totem indicators ------------------------------------------
|
|
-- Totem detection is intentionally nameplate/name based. It never reads or
|
|
-- modifies the aura cache, combat log timers or CC tracking.
|
|
|
|
local TOTEM_DEFS = {
|
|
{ key = "earthbind", spellID = 2484, names = { "Earthbind Totem", "Totem der Erdbindung" }, texture = "Interface\\Icons\\Spell_Nature_StrengthOfEarthTotem02" },
|
|
{ key = "stoneskin", spellID = 8071, names = { "Stoneskin Totem", "Totem der Steinhaut" }, texture = "Interface\\Icons\\Spell_Nature_StoneSkinTotem" },
|
|
{ key = "strengthofearth", spellID = 8075, names = { "Strength of Earth Totem", "Totem der Erdstärke" }, texture = "Interface\\Icons\\Spell_Nature_EarthBindTotem" },
|
|
{ key = "tremor", spellID = 8143, names = { "Tremor Totem", "Totem des Erdstosses", "Totem des Erdstoßes" }, texture = "Interface\\Icons\\Spell_Nature_TremorTotem" },
|
|
{ key = "grounding", spellID = 8177, names = { "Grounding Totem", "Totem der Erdung" }, texture = "Interface\\Icons\\Spell_Nature_GroundingTotem" },
|
|
{ key = "stoneclaw", spellID = 5730, names = { "Stoneclaw Totem", "Totem der Steinklaue" }, texture = "Interface\\Icons\\Spell_Nature_StoneClawTotem" },
|
|
|
|
{ key = "searing", spellID = 3599, names = { "Searing Totem", "Totem der Verbrennung" }, texture = "Interface\\Icons\\Spell_Fire_SearingTotem" },
|
|
{ key = "flametongue", spellID = 8227, names = { "Flametongue Totem", "Totem der Flammenzunge" }, texture = "Interface\\Icons\\Spell_Nature_GuardianWard" },
|
|
{ key = "magma", spellID = 8190, names = { "Magma Totem", "Totem des glühenden Magmas" }, texture = "Interface\\Icons\\Spell_Fire_SelfDestruct" },
|
|
{ key = "firenova", spellID = 1535, names = { "Fire Nova Totem", "Totem der Feuernova" }, texture = "Interface\\Icons\\Spell_Fire_SealOfFire" },
|
|
{ key = "frostres", spellID = 8181, names = { "Frost Resistance Totem", "Totem des Frostwiderstands" }, texture = "Interface\\Icons\\Spell_FrostResistanceTotem_01" },
|
|
{ key = "fireres", spellID = 8184, names = { "Fire Resistance Totem", "Totem des Feuerwiderstands" }, texture = "Interface\\Icons\\Spell_FireResistanceTotem_01" },
|
|
|
|
{ key = "healingstream", spellID = 5394, names = { "Healing Stream Totem", "Totem des heilenden Flusses" }, texture = "Interface\\Icons\\INV_Spear_04" },
|
|
{ key = "manaspring", spellID = 5675, names = { "Mana Spring Totem", "Totem der Manaquelle" }, texture = "Interface\\Icons\\Spell_Nature_ManaRegenTotem" },
|
|
{ key = "poisoncleansing", spellID = 8166, names = { "Poison Cleansing Totem", "Totem der Giftreinigung" }, texture = "Interface\\Icons\\Spell_Nature_PoisonCleansingTotem" },
|
|
{ key = "diseasecleansing", spellID = 8170, names = { "Disease Cleansing Totem", "Totem der Krankheitsreinigung" }, texture = "Interface\\Icons\\Spell_Nature_DiseaseCleansingTotem" },
|
|
{ key = "manatide", spellID = 16190, names = { "Mana Tide Totem", "Totem der Manaflut" }, texture = "Interface\\Icons\\Spell_Frost_SummonWaterElemental" },
|
|
{ key = "natureres", spellID = 10595, names = { "Nature Resistance Totem", "Totem des Naturwiderstands" }, texture = "Interface\\Icons\\Spell_Nature_NatureResistanceTotem" },
|
|
|
|
{ key = "windfury", spellID = 8512, names = { "Windfury Totem", "Totem des Windzorns" }, texture = "Interface\\Icons\\Spell_Nature_Windfury" },
|
|
{ key = "sentry", spellID = 6495, names = { "Sentry Totem", "Totem des Wachens" }, texture = "Interface\\Icons\\Spell_Nature_RemoveCurse" },
|
|
{ key = "graceofair", spellID = 8835, names = { "Grace of Air Totem", "Totem der Luftanmut" }, texture = "Interface\\Icons\\Spell_Nature_InvisibilityTotem" },
|
|
{ key = "tranquilair", spellID = 25908, names = { "Tranquil Air Totem", "Totem der beruhigenden Winde" }, texture = "Interface\\Icons\\Spell_Nature_Brilliance" },
|
|
{ key = "windwall", spellID = 15107, names = { "Windwall Totem", "Totem der Windmauer" }, texture = "Interface\\Icons\\Spell_Nature_EarthBind" },
|
|
}
|
|
|
|
|
|
local function CleanName(text)
|
|
if not text then return nil end
|
|
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
|
text = string.gsub(text, "|r", "")
|
|
text = string.gsub(text, "^%s+", "")
|
|
text = string.gsub(text, "%s+$", "")
|
|
if text == "" then return nil end
|
|
return text
|
|
end
|
|
|
|
local function NormalizeTotemName(text)
|
|
text = CleanName(text)
|
|
if not text then return nil end
|
|
|
|
text = string.lower(text)
|
|
-- Vanilla totem NPCs often carry their spell rank in the NPC name, e.g.
|
|
-- "Searing Totem VI" or "Windfury Totem II". Match the base totem name.
|
|
text = string.gsub(text, "%s+rank%s+%d+$", "")
|
|
text = string.gsub(text, "%s+%d+$", "")
|
|
text = string.gsub(text, "%s+[ivxlcdm]+$", "")
|
|
text = string.gsub(text, "%s+$", "")
|
|
return text
|
|
end
|
|
|
|
local function AddName(def, name)
|
|
if not name or name == "" then return end
|
|
local normalized = NormalizeTotemName(name)
|
|
local i
|
|
for i = 1, table.getn(def.names) do
|
|
if NormalizeTotemName(def.names[i]) == normalized then return end
|
|
end
|
|
table.insert(def.names, name)
|
|
end
|
|
|
|
local definitionsResolved = false
|
|
local TOTEM_NAME_LOOKUP = {}
|
|
local function RebuildTotemNameLookup()
|
|
local key
|
|
for key in pairs(TOTEM_NAME_LOOKUP) do TOTEM_NAME_LOOKUP[key] = nil end
|
|
|
|
local i, n
|
|
for i = 1, table.getn(TOTEM_DEFS) do
|
|
local def = TOTEM_DEFS[i]
|
|
for n = 1, table.getn(def.names) do
|
|
local normalized = NormalizeTotemName(def.names[n])
|
|
if normalized then TOTEM_NAME_LOOKUP[normalized] = def end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ResolveDefinitions()
|
|
if definitionsResolved then return end
|
|
definitionsResolved = true
|
|
|
|
if SpellInfo then
|
|
local i
|
|
for i = 1, table.getn(TOTEM_DEFS) do
|
|
local def = TOTEM_DEFS[i]
|
|
local ok, name, _, texture = pcall(SpellInfo, def.spellID)
|
|
if ok then
|
|
AddName(def, name)
|
|
if texture and texture ~= "" then def.texture = texture end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Build once after localized spell names have been added. Normal nameplates
|
|
-- can then reject in O(1) instead of re-normalizing every known totem name.
|
|
RebuildTotemNameLookup()
|
|
end
|
|
|
|
local GENERIC_TOTEM_DEF = {
|
|
key = "generic",
|
|
names = {},
|
|
texture = "Interface\\Icons\\Spell_Nature_StoneClawTotem",
|
|
}
|
|
|
|
local function FindTotemDefinition(name)
|
|
local normalized = NormalizeTotemName(name)
|
|
if not normalized then return nil end
|
|
|
|
local known = TOTEM_NAME_LOOKUP[normalized]
|
|
if known then return known end
|
|
|
|
-- Future/custom server totems still get icon-only treatment even when BNP
|
|
-- does not yet know their exact spell icon. Only accept "totem" as a
|
|
-- complete word though: player names such as "Totemtiger" must never be
|
|
-- mistaken for a totem just because they contain those five letters.
|
|
if normalized == "totem"
|
|
or string.sub(normalized, 1, 6) == "totem "
|
|
or string.sub(normalized, -6) == " totem"
|
|
or string.find(normalized, " totem ", 1, true) then
|
|
return GENERIC_TOTEM_DEF
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function GetPlateName(plate)
|
|
if plate and plate.name and plate.name.GetText then
|
|
local name = CleanName(plate.name:GetText())
|
|
if name then return name end
|
|
end
|
|
|
|
if plate and plate.GetName and UnitName then
|
|
local token = plate:GetName(1)
|
|
if token then
|
|
local ok, name = pcall(UnitName, token)
|
|
if ok then return CleanName(name) end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function PositionIndicator(plate, indicator)
|
|
if not plate or not indicator then return end
|
|
indicator:ClearAllPoints()
|
|
|
|
-- Icon-only mode: replace the visible nameplate at its original position.
|
|
if plate.healthbar then
|
|
indicator:SetPoint("CENTER", plate.healthbar, "CENTER", 0, 0)
|
|
else
|
|
indicator:SetPoint("CENTER", plate, "CENTER", 0, 0)
|
|
end
|
|
end
|
|
|
|
local function RememberAndHideVisual(object, state)
|
|
if not object or object == state.indicator then return end
|
|
if not object.SetAlpha or not object.GetAlpha then return end
|
|
|
|
if state.alpha[object] == nil then
|
|
state.alpha[object] = object:GetAlpha()
|
|
end
|
|
object:SetAlpha(0)
|
|
end
|
|
|
|
local TOTEM_VISUAL_RESCAN_INTERVAL = 0.50
|
|
|
|
local function CaptureTotemPlateVisuals(plate, state, now)
|
|
if not plate or not state then return end
|
|
|
|
-- Capturing every projected region/child allocates temporary Lua tables.
|
|
-- Doing that every rendered frame produced needless GC pressure in BGs with
|
|
-- several totems. Re-scan only occasionally so late-created BNP children
|
|
-- (castbar/combo/etc.) are still discovered without per-frame allocations.
|
|
local regions = { plate:GetRegions() }
|
|
local i
|
|
for i = 1, table.getn(regions) do
|
|
RememberAndHideVisual(regions[i], state)
|
|
end
|
|
|
|
local children = { plate:GetChildren() }
|
|
for i = 1, table.getn(children) do
|
|
RememberAndHideVisual(children[i], state)
|
|
end
|
|
|
|
state.nextCapture = (now or GetTime()) + TOTEM_VISUAL_RESCAN_INTERVAL
|
|
end
|
|
|
|
local function SuppressTotemPlateVisuals(plate, forceCapture)
|
|
if not plate then return end
|
|
|
|
local indicator = plate.BNPTotemIndicator
|
|
local state = plate.BNPTotemVisualState
|
|
local now = GetTime()
|
|
if not state then
|
|
state = { alpha = {}, indicator = indicator, nextCapture = 0 }
|
|
plate.BNPTotemVisualState = state
|
|
else
|
|
state.indicator = indicator
|
|
end
|
|
|
|
-- Important PvP totems should remain clearly visible even when BNP's
|
|
-- non-target alpha option would normally fade the underlying nameplate.
|
|
if plate.SetAlpha and plate.GetAlpha and plate:GetAlpha() ~= 1 then plate:SetAlpha(1) end
|
|
|
|
if forceCapture or not state.nextCapture or now >= state.nextCapture then
|
|
CaptureTotemPlateVisuals(plate, state, now)
|
|
end
|
|
|
|
-- Blizzard can restore alpha on its native regions every frame. Re-enforce
|
|
-- only the already-cached objects here; this loop allocates nothing.
|
|
local object
|
|
for object in pairs(state.alpha or {}) do
|
|
if object ~= indicator and object.GetAlpha and object.SetAlpha and object:GetAlpha() ~= 0 then
|
|
object:SetAlpha(0)
|
|
end
|
|
end
|
|
|
|
-- BNPTotemIndicator is itself a direct child of the plate.
|
|
if indicator and indicator.SetAlpha and indicator.GetAlpha and indicator:GetAlpha() ~= 1 then
|
|
indicator:SetAlpha(1)
|
|
end
|
|
end
|
|
|
|
local function RestoreTotemPlateVisuals(plate)
|
|
if not plate then return end
|
|
local state = plate.BNPTotemVisualState
|
|
if not state then return end
|
|
|
|
local object, alpha
|
|
for object, alpha in pairs(state.alpha or {}) do
|
|
if object and object.SetAlpha then
|
|
object:SetAlpha(alpha or 1)
|
|
end
|
|
end
|
|
|
|
plate.BNPTotemVisualState = nil
|
|
end
|
|
|
|
-- Blizzard can restore parts of a projected nameplate every frame. Once a
|
|
-- plate has been identified as a totem, suppress every normal visual again
|
|
-- after the plate's native/fullalpha OnUpdate has run. The replacement icon
|
|
-- remains a child of the plate and is explicitly excluded, so it keeps the
|
|
-- same proven projection/positioning path as the working detection version.
|
|
local function InstallTotemVisualGuard(plate)
|
|
if not plate or plate.BNPTotemVisualGuard then return end
|
|
|
|
local oldOnUpdate = plate:GetScript("OnUpdate")
|
|
plate:SetScript("OnUpdate", function()
|
|
if oldOnUpdate then oldOnUpdate() end
|
|
local current = this or plate
|
|
if not current then return end
|
|
|
|
if current.BNPTotemLastKey and
|
|
BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled() then
|
|
SuppressTotemPlateVisuals(current)
|
|
if current.BNPTotemIndicator then
|
|
if current.BNPTotemIndicator.GetAlpha and current.BNPTotemIndicator:GetAlpha() ~= 1 then
|
|
current.BNPTotemIndicator:SetAlpha(1)
|
|
end
|
|
if not current.BNPTotemIndicator:IsShown() then current.BNPTotemIndicator:Show() end
|
|
end
|
|
elseif current.BNPTotemVisualState then
|
|
RestoreTotemPlateVisuals(current)
|
|
end
|
|
end)
|
|
|
|
plate.BNPTotemVisualGuard = true
|
|
end
|
|
|
|
local function InstallTotemHideGuard(plate)
|
|
if not plate or plate.BNPTotemHideGuard then return end
|
|
|
|
local oldOnHide = plate:GetScript("OnHide")
|
|
plate:SetScript("OnHide", function()
|
|
if oldOnHide then oldOnHide() end
|
|
local current = this or plate
|
|
if not current then return end
|
|
|
|
if current.BNPTotemIndicator then current.BNPTotemIndicator:Hide() end
|
|
RestoreTotemPlateVisuals(current)
|
|
current.BNPTotemLastName = nil
|
|
current.BNPTotemLastKey = nil
|
|
end)
|
|
|
|
plate.BNPTotemHideGuard = true
|
|
end
|
|
|
|
local function ApplyIndicatorLayout(plate, force)
|
|
local indicator = plate and plate.BNPTotemIndicator
|
|
if not indicator then return end
|
|
|
|
local size = BNP.GetTotemIconSize and BNP:GetTotemIconSize() or 24
|
|
local level = plate.GetFrameLevel and ((plate:GetFrameLevel() or 0) + 2) or 2
|
|
|
|
if not force and
|
|
indicator.BNPLastSize == size and
|
|
indicator.BNPLastFrameLevel == level then
|
|
return
|
|
end
|
|
|
|
indicator.BNPLastSize = size
|
|
indicator.BNPLastFrameLevel = level
|
|
indicator:SetWidth(size)
|
|
indicator:SetHeight(size)
|
|
if indicator.SetFrameLevel then indicator:SetFrameLevel(level) end
|
|
PositionIndicator(plate, indicator)
|
|
end
|
|
|
|
local function CreateIndicator(plate)
|
|
if not plate or plate.BNPTotemIndicator then return end
|
|
|
|
local indicator = CreateFrame("Frame", nil, plate)
|
|
local size = BNP.GetTotemIconSize and BNP:GetTotemIconSize() or 24
|
|
indicator:SetWidth(size)
|
|
indicator:SetHeight(size)
|
|
indicator:SetFrameLevel((plate.GetFrameLevel and plate:GetFrameLevel() or 0) + 2)
|
|
|
|
local border = indicator:CreateTexture(nil, "BACKGROUND")
|
|
border:SetTexture(0, 0, 0, 1)
|
|
border:SetPoint("TOPLEFT", indicator, "TOPLEFT", -1, 1)
|
|
border:SetPoint("BOTTOMRIGHT", indicator, "BOTTOMRIGHT", 1, -1)
|
|
indicator.border = border
|
|
|
|
local texture = indicator:CreateTexture(nil, "ARTWORK")
|
|
texture:SetAllPoints(indicator)
|
|
texture:SetTexCoord(0.07, 0.93, 0.07, 0.93)
|
|
indicator.texture = texture
|
|
|
|
indicator:Hide()
|
|
plate.BNPTotemIndicator = indicator
|
|
ApplyIndicatorLayout(plate, true)
|
|
end
|
|
|
|
local function UpdateIndicator(plate, force)
|
|
if not plate then return end
|
|
|
|
local indicator = plate.BNPTotemIndicator
|
|
|
|
-- Disabled means zero detection/render work and, importantly, no indicator
|
|
-- frame needs to exist on ordinary nameplates.
|
|
if not (BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()) then
|
|
if indicator then indicator:Hide() end
|
|
RestoreTotemPlateVisuals(plate)
|
|
plate.BNPTotemLastName = nil
|
|
plate.BNPTotemLastKey = nil
|
|
return
|
|
end
|
|
|
|
ResolveDefinitions()
|
|
local name = GetPlateName(plate)
|
|
|
|
-- Nameplate identity is stable most of the time. Reuse the previous totem
|
|
-- decision and avoid normalization/player checks on every libnameplate tick.
|
|
if not force and name == plate.BNPTotemLastName then
|
|
if plate.BNPTotemLastKey then
|
|
if not indicator then
|
|
CreateIndicator(plate)
|
|
indicator = plate.BNPTotemIndicator
|
|
end
|
|
InstallTotemVisualGuard(plate)
|
|
ApplyIndicatorLayout(plate, false)
|
|
if indicator and not indicator:IsShown() then indicator:Show() end
|
|
SuppressTotemPlateVisuals(plate)
|
|
else
|
|
if indicator and indicator:IsShown() then indicator:Hide() end
|
|
RestoreTotemPlateVisuals(plate)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Hard safety guard: a player nameplate can never be a shaman totem.
|
|
local token = plate.GetName and plate:GetName(1) or nil
|
|
if token and UnitIsPlayer and UnitIsPlayer(token) then
|
|
if indicator then indicator:Hide() end
|
|
RestoreTotemPlateVisuals(plate)
|
|
plate.BNPTotemLastName = name
|
|
plate.BNPTotemLastKey = nil
|
|
return
|
|
end
|
|
|
|
plate.BNPTotemLastName = name
|
|
plate.BNPTotemLastKey = nil
|
|
|
|
local def = FindTotemDefinition(name)
|
|
if not def then
|
|
if indicator then indicator:Hide() end
|
|
RestoreTotemPlateVisuals(plate)
|
|
return
|
|
end
|
|
|
|
-- Lazy creation: normal mobs never receive the replacement frame or the
|
|
-- per-frame visual guard. They are installed only after a real totem match.
|
|
if not indicator then
|
|
CreateIndicator(plate)
|
|
indicator = plate.BNPTotemIndicator
|
|
end
|
|
if not indicator then return end
|
|
|
|
InstallTotemVisualGuard(plate)
|
|
ApplyIndicatorLayout(plate, force)
|
|
|
|
plate.BNPTotemLastKey = def.key
|
|
indicator.texture:SetTexture(def.texture)
|
|
indicator:Show()
|
|
SuppressTotemPlateVisuals(plate, force)
|
|
end
|
|
|
|
function BNP:RefreshTotemIndicators()
|
|
ResolveDefinitions()
|
|
local plate
|
|
for plate in pairs(self.plates or {}) do
|
|
if plate then UpdateIndicator(plate, true) end
|
|
end
|
|
end
|
|
|
|
-- Public table keeps the totem definitions centralized and easy to extend.
|
|
BNP.TotemIndicatorDefinitions = TOTEM_DEFS
|
|
|
|
table.insert(BNP.libnameplate.OnInit, function(plate)
|
|
-- Hide cleanup is event-driven and cheap; the expensive per-frame visual
|
|
-- guard is installed lazily only if this plate is actually a totem.
|
|
InstallTotemHideGuard(plate)
|
|
UpdateIndicator(plate, true)
|
|
end)
|
|
|
|
table.insert(BNP.libnameplate.OnShow, function(plate)
|
|
InstallTotemHideGuard(plate)
|
|
plate.BNPTotemLastName = nil
|
|
plate.BNPTotemLastKey = nil
|
|
UpdateIndicator(plate, true)
|
|
end)
|
|
|
|
table.insert(BNP.libnameplate.OnUpdate, function(plate)
|
|
UpdateIndicator(plate, false)
|
|
end)
|