Delete modules directory
This commit is contained in:
@@ -1,606 +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 {}
|
||||
|
||||
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 = 5
|
||||
|
||||
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
|
||||
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
|
||||
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 nil
|
||||
end
|
||||
|
||||
local function GetTargetGUID()
|
||||
local exists, guid = UnitExists("target")
|
||||
if exists and guid then return guid end
|
||||
return nil
|
||||
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 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
|
||||
|
||||
for _, def in ipairs(AURA_DEFS) do
|
||||
if AuraMatches(def, spellName, texture) then
|
||||
return def, texture, spellID
|
||||
end
|
||||
end
|
||||
|
||||
return nil, 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 same spell ID on that exact SuperWoW GUID.
|
||||
BNP.pendingAuras = BNP.pendingAuras or {}
|
||||
local PENDING_TIMEOUT = 2.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 ConfirmedSpellOnUnit(unit, spellID)
|
||||
if not unit or not spellID then return false end
|
||||
local i
|
||||
for i = 1, 32 do
|
||||
local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i)
|
||||
if not texture then break end
|
||||
if auraSpellID == spellID then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
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 = 0
|
||||
aura.confirmedAt = GetTime()
|
||||
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 ConfirmedSpellOnUnit(unit, pending.spellID) then
|
||||
CommitPendingAura(guid, key, pending)
|
||||
pendingForGUID[key] = nil
|
||||
elseif now - pending.created > PENDING_TIMEOUT then
|
||||
-- Nothing landed: resist, miss, immune, evade or otherwise failed.
|
||||
pendingForGUID[key] = nil
|
||||
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 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 or not targetGUID then 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
|
||||
|
||||
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
|
||||
|
||||
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(),
|
||||
}
|
||||
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 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 function SyncAuraRemoval(unit, guid, now)
|
||||
if not unit or not guid 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
|
||||
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)
|
||||
if age >= REMOVAL_GRACE and not present[aura.spellID] then
|
||||
cache[key] = nil
|
||||
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
|
||||
|
||||
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 = GetPlateGUID(plate)
|
||||
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
|
||||
|
||||
if remaining and remaining > 0 then
|
||||
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 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("")
|
||||
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 doRemovalScan = removalElapsed >= REMOVAL_SCAN_INTERVAL
|
||||
if doRemovalScan then removalElapsed = 0 end
|
||||
|
||||
local targetGUID = nil
|
||||
if hasPending 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 doRemovalScan and targetGUID then
|
||||
SyncAuraRemoval("target", targetGUID, now)
|
||||
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 doRemovalScan) then
|
||||
local guid = GetPlateGUID(plate)
|
||||
local token = GetUnitTokenForPlate(plate)
|
||||
|
||||
if guid and token then
|
||||
if hasPending then
|
||||
ConfirmPendingForUnit(token, guid, now)
|
||||
end
|
||||
if doRemovalScan then
|
||||
SyncAuraRemoval(token, guid, now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
UpdatePlate(plate)
|
||||
end
|
||||
|
||||
if hasPending then
|
||||
CleanupPending(now)
|
||||
end
|
||||
end)
|
||||
@@ -1,212 +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())
|
||||
bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -BAR_GAP)
|
||||
bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 0, -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("RIGHT", bar, "LEFT", -2, 0)
|
||||
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 updater = CreateFrame("Frame")
|
||||
updater:SetScript("OnUpdate", function()
|
||||
-- Castbar animation is intentionally updated every rendered frame.
|
||||
-- Event/GUID detection remains event-driven; only the cheap visual progress
|
||||
-- interpolation runs here, so the bar no longer advances in visible 20ms steps.
|
||||
local enabled = Enabled()
|
||||
local now = GetTime()
|
||||
local plate
|
||||
|
||||
for plate in pairs(BNP.plates) do
|
||||
if enabled and plate:IsShown() then
|
||||
local guid = GetPlateGUID(plate)
|
||||
local cast = guid and casts[guid]
|
||||
|
||||
if cast and now < cast.endTime then
|
||||
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(plate:GetAlpha())
|
||||
|
||||
if bar.icon then
|
||||
if cast.texture then
|
||||
bar.icon.texture:SetTexture(cast.texture)
|
||||
bar.icon:SetAlpha(plate:GetAlpha())
|
||||
bar.icon:Show()
|
||||
else
|
||||
bar.icon:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
bar:Show()
|
||||
end
|
||||
else
|
||||
if guid and cast and now >= cast.endTime then
|
||||
casts[guid] = nil
|
||||
end
|
||||
Hide(plate)
|
||||
end
|
||||
else
|
||||
Hide(plate)
|
||||
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
|
||||
@@ -1,147 +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
|
||||
|
||||
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)
|
||||
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.05 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
|
||||
if not token or not guid or guid ~= plate.BNPClassGUID or not UnitIsPlayer(token) then
|
||||
ClearClassCache(plate)
|
||||
else
|
||||
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
|
||||
elseif token and guid and UnitIsPlayer(token) then
|
||||
ApplyClassColor(plate)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -1,70 +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
|
||||
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 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,153 +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 RememberNormalColor(plate, bar)
|
||||
if plate.BNPTankActive then return end
|
||||
local r, g, b, a = bar:GetStatusBarColor()
|
||||
if r then plate.BNPNormalColor = { r, g, b, a or 1 } end
|
||||
end
|
||||
|
||||
local function RestoreNormalColor(plate, bar)
|
||||
local c = plate.BNPNormalColor
|
||||
if c then bar:SetStatusBarColor(c[1], c[2], c[3], c[4] or 1) 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
|
||||
RestoreNormalColor(plate, bar)
|
||||
return
|
||||
end
|
||||
|
||||
local myGUID = GetPlayerGUID()
|
||||
if not myGUID then return end
|
||||
|
||||
RememberNormalColor(plate, bar)
|
||||
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
|
||||
|
||||
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.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
|
||||
@@ -1,372 +0,0 @@
|
||||
BNP = BNP 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
|
||||
Reference in New Issue
Block a user