diff --git a/modules/auras.lua b/modules/auras.lua deleted file mode 100644 index d8976bb..0000000 --- a/modules/auras.lua +++ /dev/null @@ -1,1442 +0,0 @@ -if not BNP.libnameplate then return end -if not CombatLogAdd or not SpellInfo then return end - -local _, playerClass = UnitClass("player") -local UI = BNP.UI or {} -local ICON_SIZE = UI.ICON_SIZE or 18 -local function GetIconSize() - return (BNP.GetIconSize and BNP:GetIconSize()) or ICON_SIZE -end -local ICON_OFFSET_Y = UI.ICON_OFFSET_Y or 15 -local ICON_SPACING = UI.ICON_SPACING or 2 -local UPDATE_INTERVAL = 0.05 - --- SuperWoW GUID cache. Auras are learned while a unit is the current target --- and remain attached to that unit's unique GUID after the target changes. -BNP.guidAuras = BNP.guidAuras or {} -BNP.guidNames = BNP.guidNames or {} - -BNP.pvpAuraProtection = BNP.pvpAuraProtection or {} -local PVP_AURA_PROTECTION_TIME = 6.0 - -local function ProtectAuraCache(guid, reason) - if not guid then return end - BNP.pvpAuraProtection[guid] = { - untilTime = GetTime() + PVP_AURA_PROTECTION_TIME, - reason = reason, - } -end - -local function AuraCacheProtected(guid, now) - local state = guid and BNP.pvpAuraProtection[guid] - if not state then return false end - if now <= (state.untilTime or 0) then return true end - BNP.pvpAuraProtection[guid] = nil - return false -end - -local CLASS_AURAS = { - WARLOCK = BNP.WarlockAuras, - PRIEST = BNP.PriestAuras, - WARRIOR = BNP.WarriorAuras, - ROGUE = BNP.RogueAuras, - HUNTER = BNP.HunterAuras, - MAGE = BNP.MageAuras, - DRUID = BNP.DruidAuras, - SHAMAN = BNP.ShamanAuras, - PALADIN = BNP.PaladinAuras, -} -local AURA_DEFS = CLASS_AURAS[playerClass] or {} -local MAX_VISIBLE_ICONS = 8 - -local function CleanText(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", "") - return text -end - -local function NameMatches(def, name) - if not name then return false end - local lower = string.lower(name) - for _, known in ipairs(def.names) do - if lower == string.lower(known) then return true end - end - return false -end - -local function AuraMatches(def, effect, texture) - if NameMatches(def, effect) then return true end - if texture and def.textureMatch then - return string.find(string.lower(texture), def.textureMatch) and true or false - end - return false -end - -local function ScanSpellbook() - if table.getn(AURA_DEFS) == 0 then return end - - local i = 1 - while true do - local name = GetSpellName(i, BOOKTYPE_SPELL) - if not name then break end - - local texture = GetSpellTexture(i, BOOKTYPE_SPELL) - local lowerTexture = texture and string.lower(texture) or "" - - for _, def in ipairs(AURA_DEFS) do - if NameMatches(def, name) or (def.textureMatch and string.find(lowerTexture, def.textureMatch)) then - def.localizedName = name - def.texture = texture or def.texture - end - end - - i = i + 1 - end - if BNP.RefreshSpellbookDurations then BNP:RefreshSpellbookDurations() end -end - -local function CreateAuraIcon(parent, index) - local icon = CreateFrame("Frame", nil, parent) - local size = GetIconSize() - icon:SetWidth(size) - icon:SetHeight(size) - icon:SetFrameLevel(40) - - local texture = icon:CreateTexture(nil, "ARTWORK") - texture:SetAllPoints(icon) - icon.texture = texture - - local timer = icon:CreateFontString(nil, "OVERLAY") - timer:SetFont(UI.TIMER_FONT or "Fonts\\FRIZQT__.TTF", UI.TIMER_SIZE or 8, "OUTLINE") - timer:SetPoint("CENTER", icon, "CENTER", UI.TIMER_OFFSET_X or 0, UI.TIMER_OFFSET_Y or 0) - timer:SetTextColor(1, 1, 1) - icon.timer = timer - - local stack = icon:CreateFontString(nil, "OVERLAY") - stack:SetFont(UI.TIMER_FONT or "Fonts\\FRIZQT__.TTF", 8, "OUTLINE") - stack:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -1, 1) - stack:SetTextColor(1, 1, 1) - stack:SetText("") - icon.stack = stack - - icon.index = index - icon:Hide() - - return icon -end - -local function CreateAuraContainer(plate) - if not plate then return end - BNP:RegisterPlate(plate) - if plate.BNPAuraContainer then return end - - local size = GetIconSize() - local width = size * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - local container = CreateFrame("Frame", nil, plate) - container:SetWidth(width) - container:SetHeight(size) - container:SetFrameLevel(40) - - if plate.healthbar then - container:SetPoint("BOTTOM", plate.healthbar, "TOP", 0, ICON_OFFSET_Y) - else - container:SetPoint("BOTTOM", plate, "BOTTOM", 0, 24) - end - - container.icons = {} - for i = 1, MAX_VISIBLE_ICONS do - local icon = CreateAuraIcon(container, i) - if i == 1 then - icon:SetPoint("LEFT", container, "LEFT", 0, 0) - else - icon:SetPoint("LEFT", container.icons[i - 1], "RIGHT", ICON_SPACING, 0) - end - container.icons[i] = icon - end - - container:Hide() - plate.BNPAuraContainer = container -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - - -- SuperWoW stores the unit GUID/token directly on the nameplate name slot. - -- Use that exact token, just like Tank Mode and ShaguTweaks do. This avoids - -- unreliable UnitExists() resolution and, importantly, avoids same-name - -- fallback collisions between two mobs with identical names. - local token = plate:GetName(1) - if token and token ~= "" then return token end - return nil -end - -local function GetTargetGUID() - local exists, guid = UnitExists("target") - if exists and guid then return guid end - return nil -end - -local function ClearGUIDAuraState(guid) - if not guid then return end - BNP.guidAuras[guid] = nil - if BNP.pendingAuras then BNP.pendingAuras[guid] = nil end - if BNP.pvpAuraProtection then BNP.pvpAuraProtection[guid] = nil end - if BNP.darkHarvest and BNP.darkHarvest.guid == guid then BNP.darkHarvest = nil end - if BNP.guidNames then BNP.guidNames[guid] = nil end -end - -local function CleanupDeadUnit(unit, guid, now) - if not unit or not guid or not UnitIsDeadOrGhost then return false end - if AuraCacheProtected and AuraCacheProtected(guid, now) then return false end - if UnitIsDeadOrGhost(unit) then - ClearGUIDAuraState(guid) - return true - end - return false -end - - --- Fallback for players/NPCs whose nameplate disappears before the periodic --- dead-state check sees them. Vanilla combat chat reports " dies." only, --- so this fallback must NEVER clear multiple same-name GUIDs. -local function ClearAuraStateByName(name) - if not name or name == "" then return end - - -- Death chat only gives us a unit name, not the exact GUID. - -- If multiple living/cached units share that same name, clearing all of them - -- would incorrectly remove valid debuffs from the survivors. - local matchGUID = nil - local matches = 0 - local guid, knownName - - for guid, knownName in pairs(BNP.guidNames or {}) do - if knownName == name and BNP.guidAuras[guid] then - matches = matches + 1 - matchGUID = guid - - if matches > 1 then - -- Ambiguous same-name death: do nothing here. - -- Exact GUID death cleanup will handle units we can identify safely. - return - end - end - end - - if matches == 1 and matchGUID then - ClearGUIDAuraState(matchGUID) - end -end - -local deathChatFrame = CreateFrame("Frame") -deathChatFrame:RegisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH") -deathChatFrame:RegisterEvent("CHAT_MSG_COMBAT_FRIENDLY_DEATH") -deathChatFrame:SetScript("OnEvent", function() - local raw = arg1 - if not raw then return end - local _, _, deadName = string.find(raw, "^(.+) dies%.$") - if deadName then ClearAuraStateByName(deadName) end -end) - --- Only successful casts by the local player are allowed to create aura --- entries. This prevents target scans from importing debuffs cast by other --- players. SuperWoW supplies caster GUID, target GUID and spell ID. -local function GetPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then return guid end - return nil -end - -local function DefHasSpellID(def, spellID) - if not def or not spellID then return false end - - local i - if def.spellIDs then - for i = 1, table.getn(def.spellIDs) do - if def.spellIDs[i] == spellID then return true end - end - end - - if def.durations and def.durations[spellID] then return true end - return false -end - -local function FindAuraDef(spellID) - if not spellID or not SpellInfo then return nil, nil end - local spellName, _, texture = SpellInfo(spellID) - if not spellName and not texture then return nil, nil end - - -- Exact IDs are authoritative and must win over shared spell textures. - local _, def - for _, def in ipairs(AURA_DEFS) do - if DefHasSpellID(def, spellID) then - return def, texture, spellID - end - end - - for _, def in ipairs(AURA_DEFS) do - if NameMatches(def, spellName) then - return def, texture, spellID - end - end - - for _, def in ipairs(AURA_DEFS) do - if texture and def.textureMatch and string.find(string.lower(texture), def.textureMatch) then - return def, texture, spellID - end - end - - return nil, nil -end - - --- Paladin Judgements -------------------------------------------------------- --- SuperWoW reports the button press as Judgement [20271], while the target --- receives a different aura ID (e.g. Light 20344, Justice 20184, Crusader --- 20302, Wisdom 20353). Therefore normal CAST-ID == AURA-ID confirmation --- cannot work for Judgements. -local PALADIN_JUDGEMENT_CAST_ID = 20271 - -local function IsJudgementDef(def) - if not def or not def.key then return false end - return string.find(def.key, "^judgement_") and true or false -end - -local function FindJudgementAuraOnUnit(unit) - if playerClass ~= "PALADIN" or not unit then return nil end - - local i, texture, stacks, dtype, auraSpellID - for i = 1, 32 do - texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - if auraSpellID then - local auraName = nil - if SpellInfo then auraName = SpellInfo(auraSpellID) end - - local _, def - for _, def in ipairs(AURA_DEFS) do - if IsJudgementDef(def) then - local explicitMatch = false - local j - if def.spellIDs then - for j = 1, table.getn(def.spellIDs) do - if def.spellIDs[j] == auraSpellID then - explicitMatch = true - break - end - end - end - - if explicitMatch or NameMatches(def, auraName) then - return def, auraSpellID, texture - end - end - end - end - end - - return nil -end - - --- Turtle WoW Dark Harvest (spell 52552). --- It is reported by SuperWoW as CHANNEL; arg5 is channel length in ms. --- A 30% shorter tick interval means aura time advances at 1/0.70 speed. -local DARK_HARVEST_SPELL_ID = 52552 -local DARK_HARVEST_SPEED = 1 / 0.70 -local DARK_HARVEST_AURAS = { - corruption=true, curse_of_agony=true, siphon_life=true, - curse_of_doom=true, drain_life=true, drain_soul=true, -} -BNP.darkHarvest = nil - -local function StartDarkHarvest(guid, durationMS) - local now=GetTime() - local duration=tonumber(durationMS) or 8000 - if duration > 100 then duration=duration/1000 end - BNP.darkHarvest={guid=guid, lastUpdate=now, ends=now+duration} -end - -local function UpdateDarkHarvest(now) - local st=BNP.darkHarvest - if not st then return end - local untilTime=now - if untilTime > st.ends then untilTime=st.ends end - local delta=untilTime-st.lastUpdate - if delta > 0 then - local cache=BNP.guidAuras[st.guid] - if cache then - local bonus=delta*(DARK_HARVEST_SPEED-1) - local key,aura - for key,aura in pairs(cache) do - if DARK_HARVEST_AURAS[key] and type(aura)=="table" and aura.expires and aura.expires > st.lastUpdate then - aura.expires=aura.expires-bonus - end - end - end - st.lastUpdate=untilTime - end - if now >= st.ends then BNP.darkHarvest=nil end -end - - --- Cast confirmation --------------------------------------------------------- --- UNIT_CASTEVENT "CAST" means the player finished the cast, but it does not --- guarantee that the hostile aura actually landed. Resist/Miss/Immune must --- therefore never create a visible timer. --- --- We first remember the cast as "pending". It becomes a real aura only after --- UnitDebuff confirms the matching aura on that exact SuperWoW GUID. --- Never infer success from CAST alone: melee abilities can still miss/dodge, --- and hostile targets may resist or be immune after the cast event. -BNP.pendingAuras = BNP.pendingAuras or {} -BNP.pendingAoEAuras = BNP.pendingAoEAuras or {} - --- Pending failures are matched by exact SuperWoW UNIT_CASTEVENT data. --- Never use global combat-text "miss/resist" lines here: in a raid those can --- belong to another spell and previously caused unrelated pending DoTs to die. - -local function FailPendingAura(targetGUID, spellID) - if not targetGUID or not spellID then return end - local entries = BNP.pendingAuras[targetGUID] - if not entries then return end - - local def = FindAuraDef(spellID) - if def and entries[def.key] then - entries[def.key] = nil - end - - -- Generic Paladin Judgement failure. - if playerClass == "PALADIN" and spellID == PALADIN_JUDGEMENT_CAST_ID then - entries.__judgement = nil - end -end - -local PENDING_TIMEOUT = 4.0 - -local function HasPendingAuras() - local guid, entries, key, pending - for guid, entries in pairs(BNP.pendingAuras) do - for key, pending in pairs(entries) do - if type(pending) == "table" then - return true - end - end - end - return false -end - -local function GetUnitTokenForPlate(plate) - if not plate or not plate.GetName then return nil end - return plate:GetName(1) -end - -local function FindMatchingAuraOnUnit(unit, def, castSpellID) - if not unit or not def then return nil, nil, nil end - - local i - for i = 1, 32 do - local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - if auraSpellID then - if auraSpellID == castSpellID or DefHasSpellID(def, auraSpellID) then - return auraSpellID, texture, tonumber(stacks) or 0 - end - - local auraName = SpellInfo and SpellInfo(auraSpellID) or nil - if NameMatches(def, auraName) then - return auraSpellID, texture, tonumber(stacks) or 0 - end - end - - if texture and def.textureMatch and string.find(string.lower(texture), def.textureMatch) then - return auraSpellID or castSpellID, texture, tonumber(stacks) or 0 - end - end - - return nil, nil, nil -end - -local function CommitPendingAura(guid, key, pending) - if not guid or not pending then return end - - local def = pending.def - local cache = BNP.guidAuras[guid] - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - - -- Only replace mutually-exclusive curses after the new curse is confirmed - -- on the target. A resisted curse must never remove the existing one. - if def.category == "CURSE" and def.curseExclusive ~= false then - local _, oldDef - for _, oldDef in ipairs(AURA_DEFS) do - if oldDef.category == "CURSE" and oldDef.curseExclusive ~= false then - cache[oldDef.key] = nil - end - end - end - - cache._nextOrder = (cache._nextOrder or 0) + 1 - local aura = cache[key] or {} - aura.order = cache._nextOrder - aura.duration = pending.duration - aura.expires = GetTime() + pending.duration - aura.spellID = pending.spellID - aura.texture = pending.texture or def.texture - aura.stacks = tonumber(pending.stacks) or 0 - aura.confirmedAt = GetTime() - aura.missingScans = nil - cache[key] = aura -end - -local function ConfirmPendingForUnit(unit, guid, now) - if not unit or not guid then return end - local pendingForGUID = BNP.pendingAuras[guid] - if not pendingForGUID then return end - - local key, pending - for key, pending in pairs(pendingForGUID) do - if type(pending) == "table" then - if pending.judgementProbe then - local def, auraSpellID, texture = FindJudgementAuraOnUnit(unit) - - if def and auraSpellID then - local duration = def.duration - if BNP.GetSpellbookDurationForDef then - duration = BNP:GetSpellbookDurationForDef(def) or duration - end - - local confirmed = { - def = def, - spellID = auraSpellID, - texture = texture or def.texture, - duration = duration, - created = pending.created, - } - - CommitPendingAura(guid, def.key, confirmed) - pendingForGUID[key] = nil - elseif now - pending.created > PENDING_TIMEOUT then - -- No Judgement aura appeared: resist, miss, immune or failed cast. - pendingForGUID[key] = nil - end - else - local auraSpellID, auraTexture, auraStacks = FindMatchingAuraOnUnit(unit, pending.def, pending.spellID) - if auraSpellID then - local confirmed = { - def = pending.def, - spellID = auraSpellID, - texture = auraTexture or pending.texture, - duration = pending.duration, - created = pending.created, - stacks = auraStacks or 0, - } - CommitPendingAura(guid, key, confirmed) - pendingForGUID[key] = nil - elseif now - pending.created > PENDING_TIMEOUT then - -- Strict confirmation only: - -- A completed cast is NOT proof that a hostile aura landed. - -- Dodge, miss, resist, immune and evade can all occur after CAST. - -- If the exact aura never appears on this GUID, never create a timer. - pendingForGUID[key] = nil - end - end - end - end -end - -local function CleanupPending(now) - local guid, entries, key, pending - for guid, entries in pairs(BNP.pendingAuras) do - local any = false - for key, pending in pairs(entries) do - if type(pending) == "table" then - if now - pending.created > PENDING_TIMEOUT then - entries[key] = nil - else - any = true - end - end - end - if not any then BNP.pendingAuras[guid] = nil end - end -end - - -local AOE_PENDING_TIMEOUT = 1.5 - -local function QueueAoEAura(def, spellID, texture, duration) - if not def or not spellID then return end - BNP.pendingAoEAuras[def.key] = { - def = def, - spellID = spellID, - texture = texture or def.texture, - duration = duration, - created = GetTime(), - } -end - -local function ConfirmAoEAurasOnUnit(unit, guid) - if not unit or not guid then return end - local key, pending - for key, pending in pairs(BNP.pendingAoEAuras) do - if type(pending) == "table" then - local auraSpellID, auraTexture, auraStacks = FindMatchingAuraOnUnit(unit, pending.def, pending.spellID) - if auraSpellID then - CommitPendingAura(guid, pending.def.key, { - def = pending.def, - spellID = auraSpellID, - texture = auraTexture or pending.texture, - duration = pending.duration, - created = pending.created, - stacks = auraStacks or 0, - }) - end - end - end -end - -local function HasPendingAoEAuras() - local key, pending - for key, pending in pairs(BNP.pendingAoEAuras) do - if type(pending) == "table" then return true end - end - return false -end - -local function CleanupPendingAoE(now) - local key, pending - for key, pending in pairs(BNP.pendingAoEAuras) do - if type(pending) == "table" and now - (pending.created or 0) > AOE_PENDING_TIMEOUT then - BNP.pendingAoEAuras[key] = nil - end - end -end - -local castEvents = CreateFrame("Frame") -castEvents:RegisterEvent("UNIT_CASTEVENT") -castEvents:SetScript("OnEvent", function() - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = arg4 - local playerGUID = GetPlayerGUID() - - if not playerGUID or casterGUID ~= playerGUID then return end - - if eventType == "FAIL" then - FailPendingAura(targetGUID, spellID) - return - end - - -- Dark Harvest is a CHANNEL event, not CAST. Handle only this exact - -- Turtle spell here so normal aura creation remains CAST-only. - if eventType == "CHANNEL" and spellID == DARK_HARVEST_SPELL_ID then - StartDarkHarvest(targetGUID, arg5) - return - end - - if eventType ~= "CAST" then return end - - -- Shadow audit only: this never changes the existing lookup or renderer. - if BNP.AuditSpellDB then BNP:AuditSpellDB(spellID) end - - -- Paladin Judgement is a generic cast (20271). The actual debuff ID is only - -- known after it lands on the target, so defer identification to UnitDebuff. - if playerClass == "PALADIN" and spellID == PALADIN_JUDGEMENT_CAST_ID then - local pendingForGUID = BNP.pendingAuras[targetGUID] - if not pendingForGUID then - pendingForGUID = {} - BNP.pendingAuras[targetGUID] = pendingForGUID - end - - pendingForGUID.__judgement = { - judgementProbe = true, - created = GetTime(), - } - return - end - - local def, texture = FindAuraDef(spellID) - if not def then return end - - local duration = nil - if BNP.GetSpellbookDurationForDef then - duration = BNP:GetSpellbookDurationForDef(def) - end - duration = duration or (def.durations and def.durations[spellID]) or def.duration - - if def.aoe then - QueueAoEAura(def, spellID, texture, duration) - return - end - - if not targetGUID then return end - - local pendingForGUID = BNP.pendingAuras[targetGUID] - if not pendingForGUID then - pendingForGUID = {} - BNP.pendingAuras[targetGUID] = pendingForGUID - end - - pendingForGUID[def.key] = { - def = def, - spellID = spellID, - texture = texture or def.texture, - duration = duration, - created = GetTime(), - } - - -- Fast path: if the cast target is still our current target, try to confirm - -- immediately. The normal pending loop remains as the retry path. - local currentTargetGUID = GetTargetGUID() - if currentTargetGUID and currentTargetGUID == targetGUID then - ConfirmPendingForUnit("target", targetGUID, GetTime()) - end -end) - - - --- Paladin Judgement melee refresh ------------------------------------------ --- Vanilla Judgement debuffs refresh when the judging Paladin lands a melee --- strike on that target. The aura itself does not get re-cast, so UNIT_CASTEVENT --- cannot update our timer. Refresh only an already-confirmed OWN Judgement. -local function RefreshOwnJudgementForGUID(guid) - if playerClass ~= "PALADIN" or not guid then return end - - local cache = BNP.guidAuras[guid] - if not cache then return end - - local _, def - for _, def in ipairs(AURA_DEFS) do - if IsJudgementDef(def) then - local aura = cache[def.key] - if type(aura) == "table" and aura.expires then - local duration = aura.duration or def.duration or 10 - aura.duration = duration - aura.expires = GetTime() + duration - aura.confirmedAt = GetTime() - aura.missingScans = nil - end - end - end -end - --- SuperWoW gives us the target GUID of the player's current target. For normal --- auto-attacks, the combat text event is sufficient to know a successful melee --- hit occurred; matching the current target GUID keeps the refresh scoped to --- the unit actually being attacked. -local judgementMeleeFrame = CreateFrame("Frame") -judgementMeleeFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS") -judgementMeleeFrame:SetScript("OnEvent", function() - if playerClass ~= "PALADIN" then return end - - local guid = GetTargetGUID() - if not guid then return end - - -- CHAT_MSG_COMBAT_SELF_HITS only fires for landed melee attacks (hit/crit). - RefreshOwnJudgementForGUID(guid) -end) - - - --- Holy Strike also counts as a melee-style Judgement refresh on Turtle/Octo. --- It is reported via CHAT_MSG_SPELL_SELF_DAMAGE rather than the normal melee --- hit event and does not expose a useful spell ID in this client. -local holyStrikeRefreshFrame = CreateFrame("Frame") -holyStrikeRefreshFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -holyStrikeRefreshFrame:SetScript("OnEvent", function() - if playerClass ~= "PALADIN" then return end - - local raw = arg1 - if not raw then return end - - -- Only successful Holy Strike hit messages refresh the timer. - -- Miss/resist/immune lines do not match this pattern. - if not string.find(raw, "^Your Holy Strike hits ") then return end - - local guid = GetTargetGUID() - if not guid then return end - - -- Extra safety: when the target name is available, require it to be present - -- in the combat-log line so a stale/current target cannot refresh another GUID. - local targetName = UnitName("target") - if targetName and targetName ~= "" and not string.find(raw, targetName, 1, true) then - return - end - - RefreshOwnJudgementForGUID(guid) -end) - - --- Enemy Feign Death / Vanish protection. -local pvpDisappearFrame = CreateFrame("Frame") -pvpDisappearFrame:RegisterEvent("UNIT_CASTEVENT") -pvpDisappearFrame:SetScript("OnEvent", function() - local casterGUID = arg1 - local eventType = arg3 - local spellID = arg4 - - if not casterGUID or not spellID or not SpellInfo then return end - if eventType ~= "CAST" and eventType ~= "START" then return end - - local spellName = SpellInfo(spellID) - - -- Abilities that can temporarily drop target/nameplate visibility without - -- actually cleansing existing harmful effects. - local TEMPORARY_DISAPPEAR = { - ["Feign Death"] = true, - ["Vanish"] = true, - ["Shadowmeld"] = true, - } - - if spellName and TEMPORARY_DISAPPEAR[spellName] then - ProtectAuraCache(casterGUID, spellName) - end -end) - - - --- Shared/global aura exceptions --------------------------------------------- --- Most BNP debuffs are strictly "own casts only". A very small number of --- effects are globally unique on a target and are useful even when applied by --- another player. These are explicitly opted in via def.shared = true. -local function FindSharedAuraOnUnit(unit, def) - if not unit or not def then return nil, nil, nil, nil end - - local i - for i = 1, 32 do - local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = UnitDebuff(unit, i) - local texture = a1 - local auraSpellID = a4 - if not texture then break end - - local auraName = nil - if auraSpellID and SpellInfo then - auraName = SpellInfo(auraSpellID) - end - - if AuraMatches(def, auraName, texture) then - -- SuperWoW/extended clients may append duration/expiration data. - -- Probe plausible numeric return pairs without assuming one exact fork. - local duration, expires - local vals = { a5, a6, a7, a8, a9, a10 } - local n - for n = 1, table.getn(vals) - 1 do - local d = vals[n] - local e = vals[n + 1] - if type(d) == "number" and type(e) == "number" - and d > 0 and d <= 3600 and e > GetTime() then - duration = d - expires = e - break - end - end - - return auraSpellID, texture, duration, expires - end - end - - return nil, nil, nil, nil -end - -local function SyncSharedAuras(unit, guid, now) - if not unit or not guid then return end - - local _, def - for _, def in ipairs(AURA_DEFS) do - if def.shared then - local auraSpellID, texture, liveDuration, liveExpires = FindSharedAuraOnUnit(unit, def) - local cache = BNP.guidAuras[guid] - - if auraSpellID or texture then - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - - local aura = cache[def.key] - - -- Never overwrite an accurately timed own aura with the shared/live - -- fallback. Shared entries exist only when we don't own the timer. - if not aura or aura.sharedLive then - if not aura then - cache._nextOrder = (cache._nextOrder or 0) + 1 - aura = { order = cache._nextOrder } - end - - aura.sharedLive = true - aura.spellID = auraSpellID - aura.texture = texture or def.texture - aura.confirmedAt = now - - if liveExpires and liveExpires > now then - -- Best case: the client exposes the real remaining duration. - aura.duration = liveDuration - aura.expires = liveExpires - aura.sharedEstimated = nil - elseif not aura.expires then - -- Vanilla fallback: the original application time is unavailable. - -- Start a local countdown when BNP first sees the foreign mark. - -- This is intentionally marked as estimated and will still be - -- removed immediately when the real aura disappears. - aura.duration = def.duration or 120 - aura.expires = now + aura.duration - aura.sharedEstimated = true - end - - cache[def.key] = aura - end - elseif cache and cache[def.key] and cache[def.key].sharedLive then - cache[def.key] = nil - end - end - end -end - - --- Live aura removal --------------------------------------------------------- --- Timers alone are not enough for CC/roots/traps: effects may be dispelled, --- broken by damage, removed by trinkets, or otherwise end early. --- --- We NEVER create auras from this scan. It may only remove already-confirmed --- own auras from the GUID cache when their exact spell ID is no longer present. -local REMOVAL_SCAN_INTERVAL = 0.10 -local REMOVAL_GRACE = 0.35 -local NORMAL_REMOVAL_MISSING_SCANS = 4 - -local function CacheHasTrackedAuras(cache) - if not cache then return false end - local key, aura - for key, aura in pairs(cache) do - if type(aura) == "table" and aura.spellID and aura.expires then - return true - end - end - return false -end - -local EXACT_LIVE_REMOVAL = { - -- Warlock - fear=true, howl_of_terror=true, banish=true, death_coil=true, - -- Hunter - freezing_trap_effect=true, improved_wing_clip=true, improved_concussive_shot=true, - scatter_shot=true, wyvern_sting=true, entrapment=true, counterattack=true, - intimidation=true, scare_beast=true, - -- Mage - polymorph=true, frost_nova=true, counterspell_silence=true, impact=true, frostbite=true, - -- Priest - psychic_scream=true, silence=true, blackout=true, shackle_undead=true, mind_control=true, - -- Paladin - hammer_of_justice=true, repentance=true, turn_undead=true, - -- Rogue - gouge=true, kidney_shot=true, cheap_shot=true, sap=true, blind=true, kick_silenced=true, - -- Druid - entangling_roots=true, bash=true, pounce=true, hibernate=true, feral_charge_effect=true, - -- Warrior - improved_hamstring=true, intimidating_shout=true, disarm=true, charge_stun=true, - intercept_stun=true, concussion_blow=true, revenge_stun=true, pummel=true, shield_bash=true, - -- Shaman / custom roots - earthgrab=true, -} - -local function NeedsExactLiveRemoval(key) - return EXACT_LIVE_REMOVAL[key] and true or false -end - -local function SyncAuraRemoval(unit, guid, now) - if not unit or not guid then return end - if AuraCacheProtected(guid, now) then return end - local cache = BNP.guidAuras[guid] - if not CacheHasTrackedAuras(cache) then return end - - local present = {} - local i - for i = 1, 32 do - local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - if auraSpellID then - present[auraSpellID] = true - - local trackedKey, trackedAura - for trackedKey, trackedAura in pairs(cache) do - if type(trackedAura) == "table" and trackedAura.spellID == auraSpellID then - trackedAura.stacks = tonumber(stacks) or 0 - end - end - end - end - - local key, aura - for key, aura in pairs(cache) do - if type(aura) == "table" and aura.spellID and aura.expires then - -- Give the client a tiny grace period immediately after confirmation so - -- transient aura-list updates cannot remove a freshly-landed effect. - local age = now - (aura.confirmedAt or 0) - local exactRemoval = NeedsExactLiveRemoval(key) - - -- Normal DoTs on player nameplates are only safe to live-remove from the - -- CURRENT target. During target switches, non-target SuperWoW nameplate - -- tokens can briefly expose an incomplete/empty UnitDebuff list. Treating - -- that as a dispel caused valid DoTs to disappear from the GUID cache. - -- - -- CC/root/trap effects still use exact live removal on every visible plate - -- because those effects genuinely need immediate break/dispel detection. - local playerRemoval = false - if unit == "target" and UnitIsPlayer and UnitIsPlayer("target") then - local currentTargetGUID = GetTargetGUID() - playerRemoval = currentTargetGUID and currentTargetGUID == guid - end - - if age >= REMOVAL_GRACE then - if present[aura.spellID] then - -- Any positive sighting immediately resets transient-missing state. - aura.missingScans = nil - elseif exactRemoval then - -- CC/roots/traps must disappear immediately when broken/dispelled. - cache[key] = nil - elseif playerRemoval then - -- Normal player DoTs/curses use conservative removal: require - -- several consecutive missing scans before treating the aura as gone. - aura.missingScans = (aura.missingScans or 0) + 1 - - if aura.missingScans >= NORMAL_REMOVAL_MISSING_SCANS then - cache[key] = nil - end - end - end - end - end -end - -local function FormatTimer(remaining) - if not remaining or remaining <= 0 then return "" end - - -- Keep long durations compact on the small nameplate icons. - -- Examples: 300s -> 5m, 61s -> 2m, 59s -> 59. - if remaining >= 3600 then - return math.ceil(remaining / 3600) .. "h" - elseif remaining >= 60 then - return math.ceil(remaining / 60) .. "m" - end - - return tostring(math.ceil(remaining)) -end - --- Nameplate GUID stability --------------------------------------------------- --- Blizzard/SuperWoW can briefly recycle a visible plate while LOS/occlusion --- changes (for example when a wall is between the player and nearby mobs). --- During that tiny transition plate:GetName(1) may still expose the previous --- unit GUID. Rendering that GUID's cache causes "ghost DoTs" on an unrelated --- mob for a frame or two. --- --- Require the same GUID on two consecutive renderer updates before using its --- aura cache. This does not change aura tracking itself; it only gates display. -local function GetStablePlateGUID(plate) - local guid = GetPlateGUID(plate) - - if not guid then - plate.BNPAuraGUIDCandidate = nil - plate.BNPAuraGUIDStable = nil - return nil - end - - if plate.BNPAuraGUIDCandidate ~= guid then - plate.BNPAuraGUIDCandidate = guid - plate.BNPAuraGUIDStable = nil - return nil - end - - plate.BNPAuraGUIDStable = guid - return guid -end - -local function UpdatePlate(plate) - if not plate or not plate:IsShown() then return end - if not plate.BNPAuraContainer then CreateAuraContainer(plate) end - - local container = plate.BNPAuraContainer - if not container then return end - - local guid = GetStablePlateGUID(plate) - if guid and plate.name and plate.name.GetText then - local visibleName = plate.name:GetText() - if visibleName and visibleName ~= "" then BNP.guidNames[guid] = visibleName end - end - local cache = guid and BNP.guidAuras[guid] or nil - local now = GetTime() - - -- Reuse one small table per plate. Active auras are packed without gaps and - -- sorted by the order in which they were first applied to this GUID. - local active = container.activeAuras - if not active then - active = {} - container.activeAuras = active - end - local activePool = container.activeAuraPool - if not activePool then - activePool = {} - for i = 1, MAX_VISIBLE_ICONS do activePool[i] = {} end - container.activeAuraPool = activePool - end - local previousCount = table.getn(active) - for i = 1, previousCount do active[i] = nil end - local activeCount = 0 - - for _, def in ipairs(AURA_DEFS) do - local aura = cache and cache[def.key] - local remaining = aura and aura.expires and (aura.expires - now) or nil - local sharedLive = aura and aura.sharedLive - - if sharedLive or (remaining and remaining > 0) then - if sharedLive and remaining and remaining <= 0 then - remaining = nil - end - if activeCount < MAX_VISIBLE_ICONS then - activeCount = activeCount + 1 - local entry = activePool[activeCount] - active[activeCount] = entry - entry.def = def - entry.aura = aura - entry.remaining = remaining - end - elseif cache and aura then - cache[def.key] = nil - end - end - - table.sort(active, function(a, b) - return (a.aura.order or 0) < (b.aura.order or 0) - end) - - local visibleCount = table.getn(active) - local iconSize = GetIconSize() - local maxWidth = iconSize * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - local rowWidth = 0 - if visibleCount > 0 then - rowWidth = iconSize * visibleCount + ICON_SPACING * (visibleCount - 1) - end - local startX = (maxWidth - rowWidth) / 2 - - for i = 1, table.getn(container.icons) do - local icon = container.icons[i] - local entry = active[i] - - if entry then - icon:ClearAllPoints() - icon:SetPoint("LEFT", container, "LEFT", startX + (i - 1) * (iconSize + ICON_SPACING), 0) - icon.texture:SetTexture(entry.aura.texture or entry.def.texture) - - local stackCount = tonumber(entry.aura.stacks) or 0 - if icon.stack then - if stackCount > 1 then - icon.stack:SetText(tostring(stackCount)) - else - icon.stack:SetText("") - end - end - - local timerText = FormatTimer(entry.remaining) - if icon.lastTimerText ~= timerText then - icon.timer:SetText(timerText) - icon.lastTimerText = timerText - end - - icon:Show() - else - icon.lastTimerText = nil - icon.timer:SetText("") - if icon.stack then icon.stack:SetText("") end - icon:Hide() - end - end - - if visibleCount > 0 then - container:Show() - else - container:Hide() - end -end - - -function BNP:RefreshAuraLayout(plate) - if not plate or not plate.BNPAuraContainer then return end - - local container = plate.BNPAuraContainer - local size = GetIconSize() - local width = size * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - - container:SetWidth(width) - container:SetHeight(size) - - local i - for i = 1, table.getn(container.icons) do - container.icons[i]:SetWidth(size) - container.icons[i]:SetHeight(size) - end -end - -function BNP:RefreshAllAuraLayouts() - local plate - for plate in pairs(BNP.plates or {}) do - self:RefreshAuraLayout(plate) - end -end - -ScanSpellbook() -table.insert(BNP.libnameplate.OnInit, CreateAuraContainer) -table.insert(BNP.libnameplate.OnShow, CreateAuraContainer) - --- Keep the proven central update loop and the existing icon implementation. --- No CooldownFrame templates and no frames or tables are created here. -local renderer = CreateFrame("Frame") -local elapsedTotal = 0 -local removalElapsed = 0 -renderer:SetScript("OnUpdate", function() - elapsedTotal = elapsedTotal + arg1 - removalElapsed = removalElapsed + arg1 - if elapsedTotal < UPDATE_INTERVAL then return end - elapsedTotal = 0 - - local now = GetTime() - UpdateDarkHarvest(now) - - local hasPending = HasPendingAuras() - local hasPendingAoE = HasPendingAoEAuras() - local doRemovalScan = removalElapsed >= REMOVAL_SCAN_INTERVAL - if doRemovalScan then removalElapsed = 0 end - - local targetGUID = nil - if hasPending or hasPendingAoE or doRemovalScan then - targetGUID = GetTargetGUID() - end - - if hasPending and targetGUID then - -- Current target is the most reliable confirmation source. - ConfirmPendingForUnit("target", targetGUID, now) - end - - if targetGUID then - CleanupDeadUnit("target", targetGUID, now) - end - - if doRemovalScan and targetGUID then - SyncSharedAuras("target", targetGUID, now) - - if BNP.guidAuras[targetGUID] then - SyncAuraRemoval("target", targetGUID, now) - end - end - - -- The normal renderer always runs. Extra GUID/token/UnitDebuff work is only - -- performed while pending casts exist or during the throttled removal scan. - for plate in pairs(BNP.plates) do - if plate:IsShown() and (hasPending or hasPendingAoE or doRemovalScan) then - local guid = GetPlateGUID(plate) - local token = GetUnitTokenForPlate(plate) - - if guid and token then - -- Do NOT call UnitIsDeadOrGhost() on SuperWoW nameplate tokens. - -- During target changes/recycling those tokens can briefly report an - -- invalid dead state and wipe the entire GUID aura cache. - if hasPending then - ConfirmPendingForUnit(token, guid, now) - end - - if hasPendingAoE then - ConfirmAoEAurasOnUnit(token, guid) - end - - if doRemovalScan then - SyncSharedAuras(token, guid, now) - - -- Current target is already handled above via the real "target" unit. - -- Non-target exact-removal waits briefly after a target switch so a - -- transient empty UnitDebuff list cannot delete valid auras. - local currentTargetGUID = targetGUID - local isCurrentTarget = currentTargetGUID and currentTargetGUID == guid - SyncAuraRemoval(token, guid, now) - end - end - end - - UpdatePlate(plate) - end - - if hasPending then - CleanupPending(now) - end - - if hasPendingAoE then - CleanupPendingAoE(now) - end -end) - - --- Shadow Vulnerability [17794]: isolated live-aura detector. -local svElapsed = 0 -local svPresent = {} -local function SVDef() - local i - for i=1,table.getn(AURA_DEFS) do - if AURA_DEFS[i] and AURA_DEFS[i].key=="shadow_vulnerability" then return AURA_DEFS[i] end - end -end -local svFrame=CreateFrame("Frame") -svFrame:SetScript("OnUpdate",function() - if playerClass~="WARLOCK" then return end - svElapsed=svElapsed+arg1 - if svElapsed<0.10 then return end - svElapsed=0 - local ok,guid=UnitExists("target") - if not ok or not guid then return end - local found,tex,count=false,nil,0 - local i - for i=1,32 do - local t,c,dt,id=UnitDebuff("target",i) - if not t then break end - if id==17794 then found,tex,count=true,t,tonumber(c) or 0 break end - end - local def=SVDef() - if not def then return end - if found then - BNP.guidAuras[guid]=BNP.guidAuras[guid] or {} - local cache=BNP.guidAuras[guid] - local aura=cache[def.key] - if not aura then - cache._nextOrder=(cache._nextOrder or 0)+1 - aura={order=cache._nextOrder} - cache[def.key]=aura - end - if not svPresent[guid] then - aura.expires=GetTime()+10 - aura.duration=10 - end - aura.spellID=17794 - aura.texture=tex or def.texture - aura.stacks=count - aura.confirmedAt=GetTime() - aura.missingScans=nil - svPresent[guid]=true - elseif svPresent[guid] then - local cache=BNP.guidAuras[guid] - if cache then cache[def.key]=nil end - svPresent[guid]=nil - end -end) - - - --- Shadow Vulnerability refresh detector ------------------------------------- --- The aura can proc again while already active. The direct aura watcher keeps --- existence correct; this helper only refreshes its local 10s timer when a --- Shadow Bolt / Drain Soul event is followed by an active aura 17794. -local svRefreshGUID = nil -local svRefreshUntil = 0 -local svRefreshElapsed = 0 - -local function IsShadowVulnerabilityTrigger(spellID) - if not spellID or not SpellInfo then return false end - local name = SpellInfo(spellID) - return name == "Shadow Bolt" or name == "Drain Soul" -end - -local function RefreshShadowVulnerabilityIfPresent(guid) - if playerClass ~= "WARLOCK" or not guid then return false end - - local exists, currentGUID = UnitExists("target") - if not exists or not currentGUID or currentGUID ~= guid then - return false - end - - local found = false - local texture = nil - local stacks = 0 - local i - for i = 1, 32 do - local t, c, dtype, auraSpellID = UnitDebuff("target", i) - if not t then break end - if auraSpellID == 17794 then - found = true - texture = t - stacks = tonumber(c) or 0 - break - end - end - - if not found then return false end - - local def = nil - for i = 1, table.getn(AURA_DEFS) do - if AURA_DEFS[i] and AURA_DEFS[i].key == "shadow_vulnerability" then - def = AURA_DEFS[i] - break - end - end - if not def then return false end - - BNP.guidAuras[guid] = BNP.guidAuras[guid] or {} - local cache = BNP.guidAuras[guid] - local aura = cache[def.key] - - if not aura then - cache._nextOrder = (cache._nextOrder or 0) + 1 - aura = { order = cache._nextOrder } - cache[def.key] = aura - end - - aura.duration = 10 - aura.expires = GetTime() + 10 - aura.spellID = 17794 - aura.texture = texture or def.texture - aura.stacks = stacks - aura.confirmedAt = GetTime() - aura.missingScans = nil - - return true -end - -local svRefreshEvent = CreateFrame("Frame") -svRefreshEvent:RegisterEvent("UNIT_CASTEVENT") -svRefreshEvent:SetScript("OnEvent", function() - if playerClass ~= "WARLOCK" then return end - - local playerExists, playerGUID = UnitExists("player") - if not playerExists or not playerGUID or arg1 ~= playerGUID then return end - - local targetGUID = arg2 - local eventType = arg3 - local spellID = arg4 - - if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end - if not IsShadowVulnerabilityTrigger(spellID) then return end - if not targetGUID then return end - - -- Give the proc aura a brief moment to appear on the target. - svRefreshGUID = targetGUID - svRefreshUntil = GetTime() + 0.8 - svRefreshElapsed = 0 -end) - -local svRefreshFrame = CreateFrame("Frame") -svRefreshFrame:SetScript("OnUpdate", function() - if not svRefreshGUID then return end - - svRefreshElapsed = svRefreshElapsed + arg1 - if svRefreshElapsed < 0.05 then return end - svRefreshElapsed = 0 - - if GetTime() > svRefreshUntil then - svRefreshGUID = nil - return - end - - if RefreshShadowVulnerabilityIfPresent(svRefreshGUID) then - svRefreshGUID = nil - end -end) - diff --git a/modules/castbar.lua b/modules/castbar.lua deleted file mode 100644 index df14dbc..0000000 --- a/modules/castbar.lua +++ /dev/null @@ -1,264 +0,0 @@ -BNP = BNP or {} - -local casts = {} -local BAR_HEIGHT = 6 - -local function GetCastbarHeight() - if BNP.GetCastbarHeight then return BNP:GetCastbarHeight() end - return BAR_HEIGHT -end -local BAR_GAP = 2 - -local function Enabled() - if BNP.AreCastbarsEnabled then return BNP:AreCastbarsEnabled() end - return not BNP_DB or BNP_DB.castbars ~= false -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - - local token = plate:GetName(1) - if not token then return nil end - - local exists, guid = UnitExists(token) - if exists and guid then return guid end - - -- On SuperWoW the plate token itself can already be the GUID. - return token -end - -local function SpellData(spellID) - if SpellInfo and spellID then - local name, _, texture = SpellInfo(spellID) - return name or "Casting", texture - end - return "Casting", nil -end - -local function CreateCastbar(plate) - if plate.BNPCastbar then return plate.BNPCastbar end - if not plate.healthbar then return nil end - - local parent = plate.BNPScaleWrapper or plate - local bar = CreateFrame("StatusBar", nil, parent) - bar:SetHeight(GetCastbarHeight()) - -- Center the castbar under the complete Blizzard nameplate. - -- Extend beneath the level badge instead of stopping at the healthbar edge. - -- Compact row: icon + castbar together span the complete plate width. - -- The icon begins at the healthbar's left edge; the bar continues directly - -- beside it and ends beneath the level badge. - bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", BAR_HEIGHT + 1, -BAR_GAP) - bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14, -BAR_GAP) - bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") - bar:SetStatusBarColor(1.0, 0.8, 0.0) - bar:SetMinMaxValues(0, 1) - bar:SetValue(0) - bar:SetFrameLevel((plate.healthbar:GetFrameLevel() or 1) + 1) - bar:Hide() - - local bg = bar:CreateTexture(nil, "BACKGROUND") - bg:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") - bg:SetVertexColor(0.10, 0.10, 0.00, 0.85) - bg:SetAllPoints(bar) - - local text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - text:SetPoint("CENTER", bar, "CENTER", 0, 0) - text:SetTextColor(1, 1, 1) - bar.text = text - - -- Spell icon to the left of the castbar. - local icon = CreateFrame("Frame", nil, parent) - icon:SetWidth(14) - icon:SetHeight(14) - icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -BAR_GAP) - icon:SetFrameLevel(bar:GetFrameLevel()) - - local texture = icon:CreateTexture(nil, "ARTWORK") - texture:SetAllPoints(icon) - icon.texture = texture - - local border = icon:CreateTexture(nil, "OVERLAY") - border:SetTexture("Interface\\Buttons\\UI-Quickslot2") - border:SetPoint("CENTER", icon, "CENTER", 0, 0) - border:SetWidth(23) - border:SetHeight(23) - icon.border = border - - icon:Hide() - bar.icon = icon - - plate.BNPCastbar = bar - return bar -end - -local function Hide(plate) - if plate and plate.BNPCastbar then - plate.BNPCastbar:Hide() - if plate.BNPCastbar.icon then - plate.BNPCastbar.icon:Hide() - end - end -end - --- Exact SuperWoW event path used by ShaguTweaks' superwow module: --- START / CAST / CHANNEL create cast state; FAIL removes it. -local eventFrame = CreateFrame("Frame") -eventFrame:RegisterEvent("UNIT_CASTEVENT") -eventFrame:SetScript("OnEvent", function() - local guid = arg1 - local eventType = arg3 - local spellID = arg4 - local timerMS = tonumber(arg5) or 0 - - if not guid then return end - - if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then - if timerMS <= 0 then - -- Instant casts do not need a visible castbar. - casts[guid] = nil - return - end - - local name, texture = SpellData(spellID) - local now = GetTime() - local duration = timerMS / 1000 - - casts[guid] = { - name = name, - texture = texture, - startTime = now, - endTime = now + duration, - duration = duration, - channel = eventType == "CHANNEL", - } - elseif eventType == "FAIL" then - casts[guid] = nil - end -end) - -local activeCastPlates = {} - -local function MarkCastPlateActive(plate) - if not plate then return end - activeCastPlates[plate] = true -end - -local function MarkCastPlateInactive(plate) - if not plate then return end - activeCastPlates[plate] = nil - Hide(plate) -end - --- Lightweight discovery pass. This does NOT animate bars; it only checks --- visible plates for active cast state at 20 Hz. -local discovery = CreateFrame("Frame") -local discoveryElapsed = 0 -discovery:SetScript("OnUpdate", function() - discoveryElapsed = discoveryElapsed + arg1 - if discoveryElapsed < 0.05 then return end - discoveryElapsed = 0 - - if not Enabled() then - local plate - for plate in pairs(activeCastPlates) do - MarkCastPlateInactive(plate) - end - return - end - - local now = GetTime() - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() then - local guid = GetPlateGUID(plate) - local cast = guid and casts[guid] - - if cast and now < cast.endTime then - MarkCastPlateActive(plate) - elseif activeCastPlates[plate] then - MarkCastPlateInactive(plate) - end - elseif activeCastPlates[plate] then - MarkCastPlateInactive(plate) - end - end -end) - --- Frame-perfect animation remains unchanged visually, but only active --- castbars are updated every rendered frame. -local updater = CreateFrame("Frame") -updater:SetScript("OnUpdate", function() - if not Enabled() then return end - - local now = GetTime() - local plate - for plate in pairs(activeCastPlates) do - if not plate:IsShown() then - MarkCastPlateInactive(plate) - else - local guid = GetPlateGUID(plate) - local cast = guid and casts[guid] - - if not cast or now >= cast.endTime then - if guid and cast and now >= cast.endTime then - casts[guid] = nil - end - MarkCastPlateInactive(plate) - else - local bar = CreateCastbar(plate) - if bar then - local current = now - cast.startTime - if current < 0 then current = 0 end - if current > cast.duration then current = cast.duration end - - bar:SetMinMaxValues(0, cast.duration) - - if cast.channel then - bar:SetValue(cast.duration - current) - else - bar:SetValue(current) - end - - bar.text:SetText(cast.name) - bar:SetAlpha(1) - - if bar.icon then - if cast.texture then - bar.icon.texture:SetTexture(cast.texture) - bar.icon:SetAlpha(1) - bar.icon:Show() - else - bar.icon:Hide() - end - end - - bar:Show() - end - end - end - end -end) - - -function BNP:RefreshCastbarHeights() - local plate - local height = GetCastbarHeight() - - for plate in pairs(BNP.plates or {}) do - if plate and plate.BNPCastbar then - plate.BNPCastbar:SetHeight(height) - end - end -end - -function BNP:SetCastbarsEnabled(enabled) - BNP_DB = BNP_DB or {} - BNP_DB.castbars = enabled and true or false - - if not enabled then - local plate - for plate in pairs(BNP.plates) do Hide(plate) end - end - - self:Print("Nameplate castbars " .. (enabled and "enabled." or "disabled.")) -end diff --git a/modules/classcolors.lua b/modules/classcolors.lua deleted file mode 100644 index 9f57734..0000000 --- a/modules/classcolors.lua +++ /dev/null @@ -1,169 +0,0 @@ -BNP = BNP or {} - -local fallbackColors = { - WARRIOR = { 0.78, 0.61, 0.43 }, - MAGE = { 0.41, 0.80, 0.94 }, - ROGUE = { 1.00, 0.96, 0.41 }, - DRUID = { 1.00, 0.49, 0.04 }, - HUNTER = { 0.67, 0.83, 0.45 }, - SHAMAN = { 0.00, 0.44, 0.87 }, - PRIEST = { 1.00, 1.00, 1.00 }, - WARLOCK = { 0.58, 0.51, 0.79 }, - PALADIN = { 0.96, 0.55, 0.73 }, -} - -local function GetColor(class) - if RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then - local c = RAID_CLASS_COLORS[class] - return c.r, c.g, c.b - end - - local c = fallbackColors[class] - if c then return c[1], c[2], c[3] end -end - -local function Enabled() - if BNP.AreClassColorsEnabled then return BNP:AreClassColorsEnabled() end - return not BNP_DB or BNP_DB.classColors ~= false -end - -local function ClearClassCache(plate) - if not plate then return end - plate.BNPClassColorApplied = nil - plate.BNPClassColorToken = nil - plate.BNPClassGUID = nil - plate.BNPClassR = nil - plate.BNPClassG = nil - plate.BNPClassB = nil -end - -local function ResolvePlateUnit(plate) - if not plate or not plate.GetName then return nil, nil end - - local token = plate:GetName(1) - if not token or not UnitExists(token) then - return token, nil - end - - local exists, guid = UnitExists(token) - if exists then - return token, guid - end - - return token, nil -end - -local function ApplyClassColor(plate) - if not Enabled() then - ClearClassCache(plate) - return false - end - - if not plate or not plate.healthbar then return false end - - local token, guid = ResolvePlateUnit(plate) - - -- BNP only owns PLAYER class colors. - -- NPC/neutral/friendly-NPC colors stay completely under Blizzard control. - if not token or not guid or not UnitIsPlayer(token) then - ClearClassCache(plate) - return false - end - - local _, class = UnitClass(token) - local r, g, b = GetColor(class) - if not r then - ClearClassCache(plate) - return false - end - - plate.BNPClassColorApplied = true - plate.BNPClassColorToken = token - plate.BNPClassGUID = guid - plate.BNPClassR = r - plate.BNPClassG = g - plate.BNPClassB = b - - plate.healthbar:SetStatusBarColor(r, g, b) - return true -end - -BNP.ApplyClassColor = ApplyClassColor - -local function EnforceClassColor(plate) - if not Enabled() then return end - if not plate or not plate.healthbar or not plate.BNPClassGUID then return end - - local token, guid = ResolvePlateUnit(plate) - - -- Never enforce a cached color after Blizzard recycled the frame. - if not token or not guid or guid ~= plate.BNPClassGUID or not UnitIsPlayer(token) then - ClearClassCache(plate) - return - end - - local r, g, b = plate.healthbar:GetStatusBarColor() - if math.abs(r - plate.BNPClassR) > 0.01 - or math.abs(g - plate.BNPClassG) > 0.01 - or math.abs(b - plate.BNPClassB) > 0.01 then - plate.healthbar:SetStatusBarColor( - plate.BNPClassR, - plate.BNPClassG, - plate.BNPClassB - ) - end -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - local current = plate or this - if not current then return end - - ClearClassCache(current) - ApplyClassColor(current) - - local bar = current.healthbar - if bar and not current.BNPClassColorHooked then - local oldValueChanged = bar:GetScript("OnValueChanged") - bar:SetScript("OnValueChanged", function() - if oldValueChanged then oldValueChanged() end - EnforceClassColor(current) - end) - current.BNPClassColorHooked = true - end - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - ClearClassCache(current) - ApplyClassColor(current) - end) -end - --- Blizzard may rewrite player colors during combat/threat updates. --- Re-apply only while the plate still resolves to the SAME player GUID. --- If a recycled frame now represents an NPC/neutral unit, drop the cache --- immediately and never touch its Blizzard color. -local elapsed = 0 -local updater = CreateFrame("Frame") -updater:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed < 0.10 then return end - elapsed = 0 - - if not Enabled() then return end - - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() and plate.healthbar then - local token, guid = ResolvePlateUnit(plate) - - if plate.BNPClassGUID then - EnforceClassColor(plate) - elseif token and guid and UnitIsPlayer(token) then - ApplyClassColor(plate) - end - end - end -end) diff --git a/modules/commands.lua b/modules/commands.lua deleted file mode 100644 index a8efa5b..0000000 --- a/modules/commands.lua +++ /dev/null @@ -1,83 +0,0 @@ -SLASH_BLIZZNAMEPLATESPLUS1 = "/bnp" -SlashCmdList["BLIZZNAMEPLATESPLUS"] = function(msg) - msg = string.lower(msg or "") - - if msg == "" or msg == "config" or msg == "options" then - if BNP.ToggleOptions then BNP:ToggleOptions() end - elseif msg == "spellprobe" or msg == "spellbook" then - if BNP.SpellbookProbe then BNP:SpellbookProbe() else BNP:Print("Spellbook probe is not loaded.") end - elseif msg == "spellprobe corruption" then - if BNP.SpellbookProbe then BNP:SpellbookProbe("corruption") end - elseif msg == "spellprobe agony" then - if BNP.SpellbookProbe then BNP:SpellbookProbe("agony") end - elseif msg == "spellprobe siphon" then - if BNP.SpellbookProbe then BNP:SpellbookProbe("siphon") end - elseif msg == "unknown" or msg == "learn" then - if BNP.PrintUnknownBeta then BNP:PrintUnknownBeta(false) else BNP:Print("Unknown collector is not loaded.") end - elseif msg == "unknown verbose" or msg == "learn verbose" then - if BNP.PrintUnknownBeta then BNP:PrintUnknownBeta(true) else BNP:Print("Unknown collector is not loaded.") end - elseif msg == "unknown reset" or msg == "learn reset" then - if BNP.ResetUnknownBeta then BNP:ResetUnknownBeta() end - BNP.unknownDirectEvents = {} - elseif msg == "auras" or msg == "targetauras" then - if BNP.PrintTargetAuraProbe then - BNP:PrintTargetAuraProbe() - else - BNP:Print("Target aura probe is not loaded.") - end - elseif msg == "direct" then - if BNP.PrintUnknownDirectEvents then - BNP:PrintUnknownDirectEvents() - else - BNP:Print("Direct event collector is not loaded.") - end - elseif msg == "spelldb" or msg == "audit" then - if BNP.PrintSpellDBAudit then BNP:PrintSpellDBAudit() else BNP:Print("SpellDB audit is not loaded.") end - elseif msg == "spelldb reset" or msg == "audit reset" then - if BNP.ResetSpellDBAudit then BNP:ResetSpellDBAudit() end - elseif msg == "guid" or msg == "guiddebug" or msg == "diag" then - if BNP.PrintGUIDDiagnostic then BNP:PrintGUIDDiagnostic() else BNP:Print("GUID diagnostics are not loaded.") end - elseif msg == "status" then - local visible = 0 - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() then visible = visible + 1 end - end - BNP:Print("registered: " .. BNP.detected .. ", visible: " .. visible .. ", test spell: " .. tostring(BNP.corruptionName or "not found")) - elseif msg == "cache" then - local name = UnitName("target") - if not name then - BNP:Print("No target selected.") - return - end - local remaining = BNP.FindCachedCorruption and BNP.FindCachedCorruption(name) - BNP:Print("Cache for " .. name .. ": " .. tostring(remaining or "no Corruption found")) - elseif msg == "castbars on" then - if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(true) end - elseif msg == "castbars off" then - if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(false) end - elseif msg == "scale" then - BNP:Print("Current nameplate scale: " .. tostring(BNP:GetNameplateScale())) - elseif msg == "scale reset" then - if BNP.ResetNameplateScale then BNP:ResetNameplateScale() end - elseif string.find(msg, "^scale%s+") then - local _, _, value = string.find(msg, "^scale%s+([0-9%.]+)") - if BNP.SetNameplateScale then BNP:SetNameplateScale(value) end - elseif msg == "classcolors on" then - BNP_DB = BNP_DB or {} - BNP_DB.classColors = true - BNP:Print("Class colors enabled.") - elseif msg == "classcolors off" then - BNP_DB = BNP_DB or {} - BNP_DB.classColors = false - BNP:Print("Class colors disabled.") - elseif msg == "debug on" then - BNP:SetDebug(true) - elseif msg == "debug off" then - BNP:SetDebug(false) - elseif msg == "debug" then - BNP:SetDebug(not BNP.debugEnabled) - else - BNP:Print("Commands: /bnp unknown, /bnp direct, /bnp spellprobe, /bnp spelldb, /bnp guid, /bnp status, /bnp castbars on/off, /bnp scale <0.5-1.5>, /bnp scale reset, /bnp classcolors on/off, /bnp debug on, /bnp debug off") - end -end diff --git a/modules/debug.lua b/modules/debug.lua deleted file mode 100644 index 7cd623a..0000000 --- a/modules/debug.lua +++ /dev/null @@ -1,43 +0,0 @@ -if not BNP.libnameplate then return end - -local function CreateMarker(plate) - if not plate then return end - BNP:RegisterPlate(plate) - - if not plate.BNPMarker then - local marker = CreateFrame("Frame", nil, plate) - marker:SetWidth(14) - marker:SetHeight(14) - marker:SetPoint("BOTTOM", plate, "TOP", 0, 34) - marker:SetFrameLevel(30) - - marker.texture = marker:CreateTexture(nil, "OVERLAY") - marker.texture:SetAllPoints(marker) - marker.texture:SetTexture("Interface\\Buttons\\WHITE8X8") - marker.texture:SetVertexColor(1, 0, 0, 1) - - marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0) - marker.text:SetText("+") - marker.text:SetTextColor(1, 1, 1) - plate.BNPMarker = marker - end - - if BNP.debugEnabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end -end - -table.insert(BNP.libnameplate.OnInit, CreateMarker) -table.insert(BNP.libnameplate.OnShow, CreateMarker) - -function BNP:SetDebug(enabled) - BNP.debugEnabled = enabled - BNP_DB = BNP_DB or {} - BNP_DB.debugEnabled = enabled - local plate - for plate in pairs(BNP.plates) do - if plate.BNPMarker then - if enabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end - end - end - BNP:Print("Debug-Marker " .. (enabled and "aktiviert" or "deaktiviert") .. ".") -end diff --git a/modules/fullalpha.lua b/modules/fullalpha.lua deleted file mode 100644 index db80e39..0000000 --- a/modules/fullalpha.lua +++ /dev/null @@ -1,242 +0,0 @@ -BNP = BNP or {} - -local NON_TARGET_ALPHA = 0.55 - -local function IsValidFrameStrata(value) - return value == "BACKGROUND" - or value == "LOW" - or value == "MEDIUM" - or value == "HIGH" - or value == "DIALOG" - or value == "FULLSCREEN" - or value == "FULLSCREEN_DIALOG" - or value == "TOOLTIP" -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - local token = plate:GetName(1) - if not token then return nil end - local exists, guid = UnitExists(token) - if exists and guid then return guid end - return token -end - -local function GetTargetGUID() - local exists, guid = UnitExists("target") - if exists and guid then return guid end - return nil -end - -local function IsForeignTagged(plate) - if not plate or not plate.GetName then return false end - local token = plate:GetName(1) - if not token then return false end - - local exists = UnitExists(token) - if not exists then return false end - - if UnitCanAttack and not UnitCanAttack("player", token) then - return false - end - - if not UnitIsTapped or not UnitIsTapped(token) then - return false - end - - if UnitIsTappedByPlayer and UnitIsTappedByPlayer(token) then - return false - end - - return true -end - -local function DesiredAlpha(plate) - -- Target focus is now indicated by a ShaguPlates-style glow instead of - -- fading every non-target nameplate. - return 1 -end - -local function SetLevelSafe(frame, level) - if frame and frame.SetFrameLevel and level then - if frame:GetFrameLevel() ~= level then - frame:SetFrameLevel(level) - end - end -end - -local function ApplyTargetFrameLevel(plate) - if not plate or not plate:IsShown() then return end - - local wrapper = plate.BNPScaleWrapper - local target = false - - if BNP.IsTargetOnTopEnabled and BNP:IsTargetOnTopEnabled() then - local targetGUID = GetTargetGUID() - local plateGUID = GetPlateGUID(plate) - target = targetGUID and plateGUID and targetGUID == plateGUID - end - - -- Never feed GetFrameStrata() values back into SetFrameStrata(). - -- Some Vanilla/SuperWoW projected frames report "UNKNOWN". - if target then - if plate.SetFrameStrata then plate:SetFrameStrata("HIGH") end - if wrapper and wrapper.SetFrameStrata then wrapper:SetFrameStrata("HIGH") end - else - if plate.SetFrameStrata then plate:SetFrameStrata("MEDIUM") end - if wrapper and wrapper.SetFrameStrata then wrapper:SetFrameStrata("MEDIUM") end - end -end - -local function ApplyForeignTagVisual(plate) - if not plate or not plate:IsShown() then return end - - local bar = plate.healthbar - if not bar or not bar.SetStatusBarColor then return end - - local foreign = IsForeignTagged(plate) - - if foreign then - if not plate.BNPForeignTagGrey then - local r, g, b, a = bar:GetStatusBarColor() - plate.BNPForeignTagOldR = r - plate.BNPForeignTagOldG = g - plate.BNPForeignTagOldB = b - plate.BNPForeignTagOldA = a - plate.BNPForeignTagGrey = true - end - - -- Neutral grey: visually distinct from Target Focus transparency. - bar:SetStatusBarColor(0.45, 0.45, 0.45, plate.BNPForeignTagOldA or 1) - elseif plate.BNPForeignTagGrey then - -- Restore the color that BNP/Shagu/class-color logic had before the mob - -- became foreign-tagged. Clear state so future color changes can be learned. - bar:SetStatusBarColor( - plate.BNPForeignTagOldR or 1, - plate.BNPForeignTagOldG or 0, - plate.BNPForeignTagOldB or 0, - plate.BNPForeignTagOldA or 1 - ) - plate.BNPForeignTagGrey = nil - plate.BNPForeignTagOldR = nil - plate.BNPForeignTagOldG = nil - plate.BNPForeignTagOldB = nil - plate.BNPForeignTagOldA = nil - end -end - -local function EnsureTargetGlow(plate) - if not plate then return nil end - if plate.BNPTargetGlow then return plate.BNPTargetGlow end - - local anchor = plate.healthbar or plate - local glow = plate:CreateTexture(nil, "BACKGROUND") - glow:SetTexture("Interface\\AddOns\\BlizzNameplatesPlus\\media\\shagu_target_glow.tga") - glow:SetPoint("CENTER", anchor, "CENTER", 10, 0) - - -- ShaguPlates uses health width + 60 and health height + 30. - -- Use the actual Blizzard/BNP bar size so this also follows BNP scaling. - local width = anchor.GetWidth and anchor:GetWidth() or 100 - local height = anchor.GetHeight and anchor:GetHeight() or 10 - glow:SetWidth(width + 59) - glow:SetHeight(height + 22) - local r, g, b = 1, 1, 1 - if BNP.GetTargetGlowColor then - r, g, b = BNP:GetTargetGlowColor() - end - glow:SetVertexColor(r, g, b, 1) - glow:Hide() - - plate.BNPTargetGlow = glow - return glow -end - -local function ApplyTargetGlow(plate) - if not plate or not plate:IsShown() then return end - - local glow = EnsureTargetGlow(plate) - if not glow then return end - - local enabled = BNP.IsTargetFocusEnabled and BNP:IsTargetFocusEnabled() - if not enabled then - glow:Hide() - return - end - - local targetGUID = GetTargetGUID() - local plateGUID = GetPlateGUID(plate) - - if targetGUID and plateGUID and targetGUID == plateGUID then - local anchor = plate.healthbar or plate - local width = anchor.GetWidth and anchor:GetWidth() or 100 - local height = anchor.GetHeight and anchor:GetHeight() or 10 - glow:ClearAllPoints() - glow:SetPoint("CENTER", anchor, "CENTER", 10, 0) - glow:SetWidth(width + 59) - glow:SetHeight(height + 22) - if BNP.GetTargetGlowColor then - local r, g, b = BNP:GetTargetGlowColor() - glow:SetVertexColor(r, g, b, 1) - end - glow:Show() - else - glow:Hide() - end -end - -local function ApplyTargetAlpha(plate) - if not plate or not plate:IsShown() then return end - local wanted = DesiredAlpha(plate) - if plate:GetAlpha() ~= wanted then - plate:SetAlpha(wanted) - end -end - -local function InstallAlphaGuard(plate) - plate = plate or this - if not plate or plate.BNPFullAlphaGuard then return end - - local oldOnUpdate = plate:GetScript("OnUpdate") - plate:SetScript("OnUpdate", function() - if oldOnUpdate then oldOnUpdate() end - ApplyTargetAlpha(this or plate) - ApplyTargetGlow(this or plate) - ApplyForeignTagVisual(this or plate) - ApplyTargetFrameLevel(this or plate) - end) - - plate.BNPFullAlphaGuard = true - ApplyTargetAlpha(plate) -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - InstallAlphaGuard(plate or this) - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - InstallAlphaGuard(current) - ApplyTargetAlpha(current) - ApplyTargetGlow(current) - ApplyForeignTagVisual(current) - ApplyTargetFrameLevel(current) - end) -end - -function BNP:RefreshTargetFocus() - local plate - for plate in pairs(self.plates or {}) do - ApplyTargetAlpha(plate) - ApplyTargetGlow(plate) - ApplyForeignTagVisual(plate) - end -end - -function BNP:RefreshTargetOnTop() - local plate - for plate in pairs(self.plates or {}) do - ApplyTargetFrameLevel(plate) - end -end diff --git a/modules/guiddebug.lua b/modules/guiddebug.lua deleted file mode 100644 index 0eedb2d..0000000 --- a/modules/guiddebug.lua +++ /dev/null @@ -1,73 +0,0 @@ --- SuperWoW GUID probe. --- This module does not alter aura tracking or rendering. It only inspects the --- extended UnitExists return value and the unit token exposed by nameplates. - -local function CleanText(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", "") - return text -end - -local function SafePlateToken(plate) - if not plate or not plate.GetName then return nil end - - -- SuperWoW extends frame:GetName(index). ShaguTweaks already uses - -- plate:GetName(1) for its SuperWoW nameplate castbar implementation. - local ok, token = pcall(function() return plate:GetName(1) end) - if ok then return token end - return nil -end - -local function UnitExistsData(unit) - if not unit then return nil, nil end - local ok, exists, guid = pcall(UnitExists, unit) - if not ok then return nil, nil end - return exists, guid -end - -function BNP:GetSuperWoWGUID(unit) - local exists, guid = UnitExistsData(unit) - if exists and guid then return guid end - return nil -end - -function BNP:GetPlateSuperWoWData(plate) - local token = SafePlateToken(plate) - local exists, guid = UnitExistsData(token) - return token, exists, guid -end - -function BNP:PrintGUIDDiagnostic() - BNP:Print("=== SuperWoW GUID Diagnose ===") - - local targetName = UnitName("target") - local targetExists, targetGUID = UnitExistsData("target") - BNP:Print("Target: " .. tostring(targetName or "kein Ziel")) - BNP:Print("UnitExists(target): " .. tostring(targetExists) .. " | GUID: " .. tostring(targetGUID)) - - local visible = 0 - local sameName = 0 - local guidMatches = 0 - local index = 0 - - for plate in pairs(BNP.plates) do - if plate and plate:IsShown() then - visible = visible + 1 - local plateName = plate.name and plate.name.GetText and CleanText(plate.name:GetText()) or nil - - if not targetName or plateName == targetName then - sameName = sameName + 1 - index = index + 1 - local token, exists, guid = BNP:GetPlateSuperWoWData(plate) - if targetGUID and guid and targetGUID == guid then guidMatches = guidMatches + 1 end - - BNP:Print("Plate " .. index .. ": " .. tostring(plateName)) - BNP:Print(" Token: " .. tostring(token) .. " | Exists: " .. tostring(exists)) - BNP:Print(" GUID: " .. tostring(guid) .. " | Target-Match: " .. tostring(targetGUID and guid == targetGUID or false)) - end - end - end - - BNP:Print("Sichtbar: " .. visible .. " | gleicher Name: " .. sameName .. " | GUID-Matches: " .. guidMatches) -end diff --git a/modules/healthpercent.lua b/modules/healthpercent.lua deleted file mode 100644 index aff9e72..0000000 --- a/modules/healthpercent.lua +++ /dev/null @@ -1,199 +0,0 @@ -BNP = BNP or {} - -local function Enabled() - return BNP.IsHealthTextEnabled and BNP:IsHealthTextEnabled() -end - -local function CreateHealthText(plate) - if plate.BNPHealthPercent then return plate.BNPHealthPercent end - if not plate or not plate.healthbar then return nil end - - -- Purely visual/read-only overlay. No hooks or writes to the healthbar. - local parent = plate.BNPScaleWrapper or plate - local text = parent:CreateFontString(nil, "OVERLAY") - text:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") - text:SetPoint("CENTER", plate.healthbar, "CENTER", 3, 0) - text:SetTextColor(1, 1, 1) - text:Hide() - - plate.BNPHealthPercent = text - return text -end - -local function GetPercentFromBar(plate) - if not plate or not plate.healthbar then return nil end - - local minValue, maxValue = plate.healthbar:GetMinMaxValues() - local value = plate.healthbar:GetValue() - - if value and minValue and maxValue and maxValue > minValue then - local pct = ((value - minValue) / (maxValue - minValue)) * 100 - return math.floor(pct + 0.5) - end - - return nil -end - -local function GetPercentFromUnit(plate) - if not plate or not plate.GetName then return nil end - - local token = plate:GetName(1) - if not token or not UnitExists(token) then return nil end - if not UnitHealth or not UnitHealthMax then return nil end - - local health = UnitHealth(token) - local maxHealth = UnitHealthMax(token) - - if health and maxHealth and maxHealth > 0 then - return math.floor((health / maxHealth) * 100 + 0.5) - end - - return nil -end - -local function GetHealthFromUnit(plate) - if not plate or not plate.GetName then return nil, nil end - - local token = plate:GetName(1) - if not token or not UnitExists(token) then return nil, nil end - if not UnitHealth or not UnitHealthMax then return nil, nil end - - local health = tonumber(UnitHealth(token)) - local maxHealth = tonumber(UnitHealthMax(token)) - - if health and maxHealth and maxHealth > 0 then - return health, maxHealth - end - - return nil, nil -end - -local function GetHealthFromBar(plate) - if not plate or not plate.healthbar then return nil, nil end - - local minValue, maxValue = plate.healthbar:GetMinMaxValues() - local value = plate.healthbar:GetValue() - - value = tonumber(value) - minValue = tonumber(minValue) - maxValue = tonumber(maxValue) - - if value and minValue and maxValue and maxValue > minValue then - return value - minValue, maxValue - minValue - end - - return nil, nil -end - -local function FormatHealth(value) - value = tonumber(value) - if not value then return nil end - if value < 0 then value = 0 end - - -- Keep boss/player values readable inside the compact Blizzard healthbar. - if value >= 1000000 then - local shown = math.floor((value / 1000000) * 10 + 0.5) / 10 - return tostring(shown) .. "m" - elseif value >= 10000 then - return tostring(math.floor(value / 1000 + 0.5)) .. "k" - elseif value >= 1000 then - local shown = math.floor((value / 1000) * 10 + 0.5) / 10 - return tostring(shown) .. "k" - end - - return tostring(math.floor(value + 0.5)) -end - -local function UpdatePlate(plate) - if not plate or not plate.healthbar then return end - - local text = plate.BNPHealthPercent - - if not Enabled() or not plate:IsShown() then - if text then text:Hide() end - return - end - - text = text or CreateHealthText(plate) - if not text then return end - - local mode = BNP:GetHealthTextMode() - local health, maxHealth = GetHealthFromUnit(plate) - - -- Fallback for clients/units where the SuperWoW unit token is temporarily - -- unavailable. This may be normalized health on stock Vanilla. - if health == nil or maxHealth == nil then - health, maxHealth = GetHealthFromBar(plate) - end - - local pct = nil - if health and maxHealth and maxHealth > 0 then - pct = math.floor((health / maxHealth) * 100 + 0.5) - else - pct = GetPercentFromBar(plate) - if pct == nil then pct = GetPercentFromUnit(plate) end - end - - if pct ~= nil then - if pct < 0 then pct = 0 end - if pct > 100 then pct = 100 end - end - - local hpText = FormatHealth(health) - local display = nil - - if mode == "percent" then - if pct ~= nil then display = tostring(pct) .. "%" end - elseif mode == "hp" then - display = hpText - elseif mode == "both" then - if hpText and pct ~= nil then - display = hpText .. " | " .. tostring(pct) .. "%" - elseif hpText then - display = hpText - elseif pct ~= nil then - display = tostring(pct) .. "%" - end - end - - if not display or display == "" then - text:Hide() - return - end - - if text.BNPLastHealthText ~= display then - text:SetText(display) - text.BNPLastHealthText = display - end - text:Show() -end - --- Intentionally simple fallback: only reads values and updates text. --- No SetScript calls on healthbar, no SetAlpha, no color changes. -local updater = CreateFrame("Frame") -local elapsed = 0 -updater:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed < 0.10 then return end - elapsed = 0 - - local plate - for plate in pairs(BNP.plates or {}) do - if plate:IsShown() then - UpdatePlate(plate) - elseif plate.BNPHealthPercent then - plate.BNPHealthPercent:Hide() - end - end -end) - -function BNP:RefreshHealthPercent() - local plate - for plate in pairs(self.plates or {}) do - if plate and plate:IsShown() then - UpdatePlate(plate) - elseif plate and plate.BNPHealthPercent then - plate.BNPHealthPercent:Hide() - end - end -end diff --git a/modules/scale.lua b/modules/scale.lua deleted file mode 100644 index 09f60e8..0000000 --- a/modules/scale.lua +++ /dev/null @@ -1,114 +0,0 @@ -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 EnsureScaleWrapper(plate) - if not plate or plate.BNPScaleWrapper then return end - - local originalWidth = plate:GetWidth() - local originalHeight = plate:GetHeight() - - local wrapper = CreateFrame("Frame", nil, plate) - wrapper:SetAllPoints(plate) - wrapper.plate = plate - wrapper.originalWidth = originalWidth - wrapper.originalHeight = originalHeight - - plate.BNPScaleWrapper = wrapper - - -- Match ShaguTweaks: move Blizzard nameplate visuals into a child frame. - -- Never SetScale() on the original WorldFrame nameplate itself, otherwise - -- its projected screen position shifts toward the unit's feet. - if plate.healthbar then - plate.healthbar:SetParent(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 - object:SetParent(wrapper) - end - end -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) - - -- 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:ApplyNameplateScaleAll() - local plate - for plate in pairs(self.plates) do - self:ApplyNameplateScale(plate) - end -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 diff --git a/modules/shagu_darkmode.lua b/modules/shagu_darkmode.lua deleted file mode 100644 index 3b02ebe..0000000 --- a/modules/shagu_darkmode.lua +++ /dev/null @@ -1,67 +0,0 @@ -BNP = BNP or {} - --- ShaguTweaks Darkened UI compatibility ------------------------------------- --- ShaguTweaks darkens a nameplate once and marks it with plate.darkened=true. --- Blizzard can later recycle that same plate and restore its original gold --- textures while the flag remains true. Shagu then skips the recycled plate. --- --- BNP does not implement its own dark mode here. It simply asks ShaguTweaks --- to run its own DarkenFrame() again whenever Blizzard reuses a nameplate. - -local pending = {} - -local function ShaguDarkModeActive() - return ShaguTweaks - and ShaguTweaks.DarkMode - and ShaguTweaks.DarkenFrame -end - -local function ReapplyShaguDarkMode(plate) - if not plate or not plate:IsShown() then return end - if not ShaguDarkModeActive() then return end - - -- Clear Shagu's one-time marker because this Blizzard plate may have been - -- recycled for another unit and had its textures reset. - plate.darkened = nil - - -- Use ShaguTweaks' own implementation/configuration as the single source of - -- truth. Its blacklist already protects StatusBars, spell icons, portraits, - -- etc. from being incorrectly darkened. - ShaguTweaks.DarkenFrame(plate) - plate.darkened = true -end - -local function QueueReapply(plate) - if not plate then return end - - -- Immediate pass handles normal OnShow. - ReapplyShaguDarkMode(plate) - - -- Blizzard may restore some nameplate texture state immediately after - -- OnShow. Do one delayed pass and then stop; no permanent scanner is needed. - pending[plate] = GetTime() + 0.08 -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - QueueReapply(plate or this) - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - QueueReapply(plate or this) - end) -end - -local updater = CreateFrame("Frame") -updater:SetScript("OnUpdate", function() - if not next(pending) then return end - - local now = GetTime() - local plate, due - for plate, due in pairs(pending) do - if now >= due then - pending[plate] = nil - ReapplyShaguDarkMode(plate) - end - end -end) diff --git a/modules/spellprobe.lua b/modules/spellprobe.lua deleted file mode 100644 index 1912da8..0000000 --- a/modules/spellprobe.lua +++ /dev/null @@ -1,177 +0,0 @@ -BNP = BNP or {} - -local wanted = { - corruption = { "Corruption", "Verderbnis" }, - agony = { "Curse of Agony", "Fluch der Pein" }, - siphon = { "Siphon Life", "Lebensentzug" }, - shadow_word_pain = { "Shadow Word: Pain", "Schattenwort: Schmerz" }, -} - -local function isWanted(name, list) - if not name then return false end - local lower = string.lower(name) - local i - for i = 1, table.getn(list) do - if lower == string.lower(list[i]) then return true end - end - return false -end - -local function normalizeNumber(value) - if not value then return nil end - value = string.gsub(value, ",", ".") - return tonumber(value) -end - --- WoW 1.12 uses Lua 5.0. Use string.find captures instead of string.match. -local function durationFromLine(text) - if not text then return nil end - local lower = string.lower(text) - local _, _, num - - -- German: 14,95 Sek. / 14.95 Sekunden - _, _, num = string.find(lower, "([0-9]+[%,%.]?[0-9]*)%s*sek") - if num then return normalizeNumber(num) end - - -- English: 14.95 sec / seconds - _, _, num = string.find(lower, "([0-9]+[%,%.]?[0-9]*)%s*sec") - if num then return normalizeNumber(num) end - - -- Generic short form: "over 14.95 s" / "14,95 s" - _, _, num = string.find(lower, "over%s+([0-9]+[%,%.]?[0-9]*)%s+s") - if num then return normalizeNumber(num) end - - return nil -end - -local function getTooltip() - local tooltip = getglobal("BNPSpellProbeTooltip") - if tooltip then return tooltip end - - tooltip = CreateFrame("GameTooltip", "BNPSpellProbeTooltip", UIParent, "GameTooltipTemplate") - tooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - return tooltip -end - -local function scanSpell(slot) - local tooltip = getTooltip() - tooltip:ClearLines() - tooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - tooltip:SetSpell(slot, BOOKTYPE_SPELL) - - local lines = {} - local bestDuration = nil - local i - for i = 1, tooltip:NumLines() do - local fs = getglobal("BNPSpellProbeTooltipTextLeft" .. i) - local text = fs and fs:GetText() - if text and text ~= "" then - table.insert(lines, text) - local parsed = durationFromLine(text) - if parsed and parsed > 0 then - bestDuration = parsed - end - end - end - - return bestDuration, lines -end - -local function findSpell(wantedNames) - local slot = 1 - local found = nil - while true do - local name, rank = GetSpellName(slot, BOOKTYPE_SPELL) - if not name then break end - if isWanted(name, wantedNames) then - found = { slot = slot, name = name, rank = rank } - -- keep scanning so the highest learned rank wins - end - slot = slot + 1 - end - return found -end - - -BNP.spellbookDurations = BNP.spellbookDurations or {} - -function BNP:RefreshSpellbookDurations() - local id - for id, names in pairs(wanted) do - local spell = findSpell(names) - if spell then - local ok, duration = pcall(scanSpell, spell.slot) - if ok and duration and duration > 0 then - BNP.spellbookDurations[id] = duration - BNP.spellbookDurations[string.lower(spell.name)] = duration - end - end - end -end - -function BNP:GetSpellbookDurationForDef(def) - if not def then return nil end - BNP.spellbookDurations = BNP.spellbookDurations or {} - - if def.key and BNP.spellbookDurations[def.key] then - return BNP.spellbookDurations[def.key] - end - - if def.names then - local i - for i = 1, table.getn(def.names) do - local key = string.lower(def.names[i]) - if BNP.spellbookDurations[key] then - return BNP.spellbookDurations[key] - end - end - - -- Generic on-demand scan: works for every class, not only the original - -- Warlock/Priest probe list. - local spell = findSpell(def.names) - if spell then - local ok, duration = pcall(scanSpell, spell.slot) - if ok and duration and duration > 0 then - if def.key then BNP.spellbookDurations[def.key] = duration end - BNP.spellbookDurations[string.lower(spell.name)] = duration - return duration - end - end - end - - return nil -end - -function BNP:SpellbookProbe(key) - self:RefreshSpellbookDurations() - local keys = {} - if key and key ~= "" and wanted[key] then - table.insert(keys, key) - else - table.insert(keys, "corruption") - table.insert(keys, "agony") - table.insert(keys, "siphon") - table.insert(keys, "shadow_word_pain") - end - - self:Print("=== Spellbook Tooltip Probe ===") - local k - for k = 1, table.getn(keys) do - local id = keys[k] - local spell = findSpell(wanted[id]) - if not spell then - self:Print(id .. ": not found in spellbook") - else - local ok, duration, lines = pcall(scanSpell, spell.slot) - if not ok then - self:Print(spell.name .. ": tooltip scan ERROR: " .. tostring(duration)) - else - self:Print(spell.name .. " " .. tostring(spell.rank or "") .. " | Slot " .. spell.slot .. " | Duration: " .. tostring(duration or "NOT DETECTED")) - local i - for i = 1, table.getn(lines) do - self:Print(" " .. i .. ": " .. lines[i]) - end - end - end - end -end diff --git a/modules/tankmode.lua b/modules/tankmode.lua deleted file mode 100644 index 3432483..0000000 --- a/modules/tankmode.lua +++ /dev/null @@ -1,164 +0,0 @@ -BNP = BNP or {} - --- Tank Mode for SuperWoW GUID nameplates. --- GREEN = hostile NPC currently targets the player. --- RED = hostile NPC currently targets another unit. --- Idle hostile NPCs keep their normal Blizzard color. - -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 - -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 - 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 end - return nil -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(guid) then return false end - return UnitCanAttack("player", guid) and true or false -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. - plate.BNPTankActive = nil - plate.BNPTankR = nil - plate.BNPTankG = nil - plate.BNPTankB = nil -end - -local function ClearTankState(plate) - if not plate then return end - 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.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 - 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 - - local myGUID = GetPlayerGUID() - if not myGUID then return end - - local targetGUID = GetTargetGUID(guid) - - if targetGUID and targetGUID == myGUID then - ApplyTankColor(plate, bar, GREEN_R, GREEN_G, GREEN_B) - elseif targetGUID then - ApplyTankColor(plate, bar, RED_R, RED_G, RED_B) - 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() - 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) - 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) - end) - - table.insert(self.libnameplate.OnUpdate, function(plate, elapsed) - local current = plate or this - if not current then return end - BNP:UpdateTankModePlate(current) - EnforceTankColor(current) - end) - - self.BNPTankModeInstalled = true -end diff --git a/modules/unknown.lua b/modules/unknown.lua deleted file mode 100644 index cbf3753..0000000 --- a/modules/unknown.lua +++ /dev/null @@ -1,498 +0,0 @@ -BNP = BNP or {} -BNP.unknownDirectEvents = BNP.unknownDirectEvents or {} - --- Beta spell collector ------------------------------------------------------- --- Purpose: help testers report missing Turtle/custom spell IDs without --- editing Lua files. It records unknown own cast IDs and unknown debuff IDs --- that newly appear shortly after one of the player's casts. --- --- It is intentionally diagnostic only: it never changes tracking/rendering. - -BNP.unknownBeta = BNP.unknownBeta or { - entries = {}, - order = {}, - recentCasts = {}, - seenByGUID = {}, - activeUntil = 0, -} - -local _, playerClass = UnitClass("player") - -local CLASS_AURAS = { - WARLOCK = BNP.WarlockAuras, - PRIEST = BNP.PriestAuras, - WARRIOR = BNP.WarriorAuras, - ROGUE = BNP.RogueAuras, - HUNTER = BNP.HunterAuras, - MAGE = BNP.MageAuras, - DRUID = BNP.DruidAuras, - SHAMAN = BNP.ShamanAuras, - PALADIN = BNP.PaladinAuras, -} -local AURA_DEFS = CLASS_AURAS[playerClass] or {} - -local function GetPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then return guid end - return nil -end - -local function GetUnitGUID(unit) - local exists, guid = UnitExists(unit) - if exists and guid then return guid end - return nil -end - -local function IsExplicitKnownID(spellID) - if not spellID then return false end - - -- Dark Harvest is handled by dedicated Warlock logic. - if spellID == 52552 then return true end - - local i, j, def - for i = 1, table.getn(AURA_DEFS) do - def = AURA_DEFS[i] - - if def.spellIDs then - for j = 1, table.getn(def.spellIDs) do - if def.spellIDs[j] == spellID then return true end - end - end - - if def.durations and def.durations[spellID] then - return true - end - end - - return false -end - -local function SpellName(spellID) - if not spellID or not SpellInfo then return "?" end - local name = SpellInfo(spellID) - return name or "?" -end - -local function AddEntry(kind, spellID, spellName, guid, trigger, confidence) - if not spellID then return end - - local key = tostring(kind) .. ":" .. tostring(spellID) - local data = BNP.unknownBeta - local old = data.entries[key] - - if old then - old.count = (old.count or 1) + 1 - old.lastSeen = GetTime() - if confidence == "TARGET" then old.confidence = confidence end - if trigger and not old.trigger then old.trigger = trigger end - return - end - - local entry = { - kind = kind, - spellID = spellID, - name = spellName or SpellName(spellID), - class = playerClass or "?", - guid = guid, - trigger = trigger, - confidence = confidence or "?", - count = 1, - firstSeen = GetTime(), - lastSeen = GetTime(), - } - - data.entries[key] = entry - table.insert(data.order, key) -end - -local function RememberCast(spellID, targetGUID, eventType) - local data = BNP.unknownBeta - local now = GetTime() - data.activeUntil = now + 8.0 - local cast = { - time = now, - spellID = spellID, - name = SpellName(spellID), - targetGUID = targetGUID, - eventType = eventType, - } - - table.insert(data.recentCasts, cast) - while table.getn(data.recentCasts) > 20 do - table.remove(data.recentCasts, 1) - end - - -- Unknown own CAST/CHANNEL IDs are useful too. Some trap/proc systems expose - -- the effect as a separate cast event before the aura is seen. - if spellID and not IsExplicitKnownID(spellID) then - AddEntry("CAST", spellID, cast.name, targetGUID, nil, "OWN") - end -end - -local function FindRecentTrigger(guid, now) - local casts = BNP.unknownBeta.recentCasts - local i - for i = table.getn(casts), 1, -1 do - local cast = casts[i] - local age = now - cast.time - - if age > 6 then - break - end - - if cast.targetGUID and guid and cast.targetGUID == guid then - return cast, "TARGET" - end - - -- Short global fallback catches effects such as traps where the placement - -- cast may not carry the eventual victim GUID. - if age <= 1.5 then - return cast, "RECENT" - end - end - - return nil, nil -end - -local function ScanUnit(unit, guid, now) - if not unit or not guid then return end - - local seen = BNP.unknownBeta.seenByGUID[guid] - if not seen then - seen = {} - BNP.unknownBeta.seenByGUID[guid] = seen - end - - local current = {} - local i - for i = 1, 32 do - local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - if auraSpellID then - current[auraSpellID] = true - - if not seen[auraSpellID] and not IsExplicitKnownID(auraSpellID) then - local trigger, confidence = FindRecentTrigger(guid, now) - if trigger then - AddEntry( - "AURA", - auraSpellID, - SpellName(auraSpellID), - guid, - tostring(trigger.name) .. " [" .. tostring(trigger.spellID) .. "]", - confidence - ) - end - end - end - end - - -- Keep only currently present aura IDs so a later re-application can be - -- detected again without growing this table forever. - BNP.unknownBeta.seenByGUID[guid] = current -end - - --- Indirect Effect Audit: traps/procs may never expose a UnitDebuff ID. -BNP.unknownBeta.combat = BNP.unknownBeta.combat or {} -local EFFECT_WINDOW = 8.0 - - -local function IsUsefulEffectLine(raw) - if not raw or raw == "" then return false end - local lower = string.lower(raw) - - -- Ignore obvious player-action/noise lines. - if string.find(lower, "you cast ", 1, true) then return false end - if string.find(lower, "auto shot", 1, true) then return false end - if string.find(lower, "login", 1, true) then return false end - - -- Keep lines that actually describe a harmful aura/effect or its periodic damage. - if string.find(lower, "afflicted by", 1, true) then return true end - if string.find(lower, "is afflicted", 1, true) then return true end - if string.find(lower, "is affected by", 1, true) then return true end - if string.find(lower, "suffers", 1, true) then return true end - if string.find(lower, "periodic", 1, true) then return true end - - return false -end - -local function AddCombatEffect(raw) - if not IsUsefulEffectLine(raw) then return end - local now=GetTime() - local casts=BNP.unknownBeta.recentCasts - local trigger=nil - local i - for i=table.getn(casts),1,-1 do - local c=casts[i] - if now-c.time > EFFECT_WINDOW then break end - if c.spellID ~= 75 then trigger=c break end - end - if not trigger then return end - if string.find(string.lower(raw),"auto shot",1,true) then return end - - local key=tostring(trigger.spellID)..":"..raw - local old=BNP.unknownBeta.combat[key] - if old then old.count=old.count+1 return end - BNP.unknownBeta.combat[key]={ - raw=raw, triggerID=trigger.spellID, triggerName=trigger.name, count=1 - } -end - -local combatFrame=CreateFrame("Frame") -local combatEvents={ - "CHAT_MSG_COMBAT_SELF_HITS", - "CHAT_MSG_SPELL_SELF_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", -} -local cei -for cei=1,table.getn(combatEvents) do combatFrame:RegisterEvent(combatEvents[cei]) end -combatFrame:SetScript("OnEvent",function() AddCombatEffect(arg1) end) - -local castFrame = CreateFrame("Frame") -castFrame:RegisterEvent("UNIT_CASTEVENT") -castFrame:SetScript("OnEvent", function() - local playerGUID = GetPlayerGUID() - if not playerGUID or arg1 ~= playerGUID then return end - - local eventType = arg3 - if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end - - RememberCast(arg4, arg2, eventType) -end) - -local scanner = CreateFrame("Frame") -local elapsed = 0 -scanner:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed < 0.35 then return end - elapsed = 0 - - local now = GetTime() - if not BNP.unknownBeta.activeUntil or now > BNP.unknownBeta.activeUntil then - return - end - - local targetGUID = GetUnitGUID("target") - if targetGUID then - ScanUnit("target", targetGUID, now) - end - - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() and plate.GetName then - local token = plate:GetName(1) - if token then - local guid = GetUnitGUID(token) - if guid then - ScanUnit(token, guid, now) - end - end - end - end -end) - -function BNP:PrintUnknownBeta(verbose) - local data = self.unknownBeta - self:Print("=== BNP Beta: Unknown Auras (" .. tostring(playerClass or "?") .. ") ===") - - local auraCount = 0 - local i - for i = 1, table.getn(data.order) do - local entry = data.entries[data.order[i]] - if entry and entry.kind == "AURA" then - auraCount = auraCount + 1 - local line = - tostring(auraCount) .. - " | AURA | ID " .. tostring(entry.spellID) .. - " | " .. tostring(entry.name) .. - " | x" .. tostring(entry.count or 1) - - if entry.confidence then - line = line .. " | " .. tostring(entry.confidence) - end - - self:Print(line) - - if entry.trigger then - self:Print(" Trigger: " .. tostring(entry.trigger)) - end - end - end - - local combatCount = 0 - local ck - for ck in pairs(data.combat or {}) do combatCount = combatCount + 1 end - - if combatCount > 0 then - self:Print("--- Indirect Effects ---") - local key, e - for key, e in pairs(data.combat) do - self:Print("EFFECT | Trigger " .. tostring(e.triggerName) .. " [" .. tostring(e.triggerID) .. "] | x" .. tostring(e.count)) - self:Print(" " .. tostring(e.raw)) - end - end - - if auraCount == 0 and combatCount == 0 then - self:Print("No unknown negative auras/effects found since login.") - end - - if verbose then - self:Print("--- Verbose: unknown player cast IDs ---") - local castCount = 0 - for i = 1, table.getn(data.order) do - local entry = data.entries[data.order[i]] - if entry and entry.kind == "CAST" then - castCount = castCount + 1 - self:Print( - tostring(castCount) .. - " | CAST | ID " .. tostring(entry.spellID) .. - " | " .. tostring(entry.name) .. - " | x" .. tostring(entry.count or 1) - ) - end - end - if castCount == 0 then - self:Print("No unknown cast IDs.") - end - else - self:Print("A screenshot of this output is enough. Raw data: /bnp unknown verbose") - end -end - -function BNP:ResetUnknownBeta() - self.unknownBeta.entries = {} - self.unknownBeta.order = {} - self.unknownBeta.recentCasts = {} - self.unknownBeta.seenByGUID = {} - self.unknownBeta.combat = {} - self.unknownBeta.activeUntil = 0 - self:Print("Unknown spell/effect list cleared.") -end - - - --- Target Aura Probe ---------------------------------------------------------- --- Diagnostic helper for proc/talent auras that the normal unknown collector --- may miss because they were already present in its seen-state. -function BNP:PrintTargetAuraProbe() - local exists, guid = UnitExists("target") - if not exists or not guid then - self:Print("Target Aura Probe: no target selected.") - return - end - - self:Print("=== BNP Target Negative Auras ===") - self:Print("Target: " .. tostring(UnitName("target") or "?") .. " | GUID " .. tostring(guid)) - - local count = 0 - local i - for i = 1, 32 do - local texture, stacks, dtype, auraSpellID = UnitDebuff("target", i) - if not texture then break end - - count = count + 1 - local name = auraSpellID and SpellName(auraSpellID) or "?" - self:Print( - tostring(count) .. - " | ID " .. tostring(auraSpellID or "?") .. - " | " .. tostring(name) .. - " | stacks " .. tostring(stacks or 0) .. - " | type " .. tostring(dtype or "?") - ) - end - - if count == 0 then - self:Print("No negative target auras found.") - end -end - --- Direct player spell collector -------------------------------------------- --- Captures successful player spell events that do not create a persistent --- aura and therefore never appear in the normal unknown-aura collector. - -local function GetLocalPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then return guid end - return nil -end - -local function RememberDirectEvent(eventType, spellID, targetGUID, extra) - if not spellID then return end - - local name = nil - if SpellInfo then name = SpellInfo(spellID) end - name = name or ("Spell " .. tostring(spellID)) - - local key = tostring(eventType) .. ":" .. tostring(spellID) - local entry = BNP.unknownDirectEvents[key] - - if not entry then - entry = { - eventType = eventType, - spellID = spellID, - name = name, - targetGUID = targetGUID, - count = 0, - extra = extra, - last = GetTime(), - } - BNP.unknownDirectEvents[key] = entry - end - - entry.count = (entry.count or 0) + 1 - entry.targetGUID = targetGUID or entry.targetGUID - entry.extra = extra or entry.extra - entry.last = GetTime() -end - -local directFrame = CreateFrame("Frame") -directFrame:RegisterEvent("UNIT_CASTEVENT") -directFrame:SetScript("OnEvent", function() - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = arg4 - local playerGUID = GetLocalPlayerGUID() - - if not playerGUID or casterGUID ~= playerGUID then return end - if not spellID then return end - - if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then - RememberDirectEvent(eventType, spellID, targetGUID, arg5) - end -end) - -function BNP:PrintUnknownDirectEvents() - local list = {} - local key, entry - - for key, entry in pairs(self.unknownDirectEvents or {}) do - table.insert(list, entry) - end - - table.sort(list, function(a, b) - return (a.last or 0) > (b.last or 0) - end) - - if table.getn(list) == 0 then - self:Print("--- Direct Player Events: none captured ---") - return - end - - self:Print("--- Direct Player Events ---") - - local i, e - for i = 1, table.getn(list) do - e = list[i] - self:Print( - tostring(i) .. " | " .. - tostring(e.eventType) .. " | ID " .. - tostring(e.spellID) .. " | " .. - tostring(e.name) .. " | x" .. - tostring(e.count or 1) - ) - end -end