diff --git a/modules/auras.lua b/modules/auras.lua deleted file mode 100644 index 21b1f5d..0000000 --- a/modules/auras.lua +++ /dev/null @@ -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) diff --git a/modules/castbar.lua b/modules/castbar.lua deleted file mode 100644 index 8982738..0000000 --- a/modules/castbar.lua +++ /dev/null @@ -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 diff --git a/modules/classcolors.lua b/modules/classcolors.lua deleted file mode 100644 index 75770ae..0000000 --- a/modules/classcolors.lua +++ /dev/null @@ -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) diff --git a/modules/commands.lua b/modules/commands.lua deleted file mode 100644 index 6cf2a94..0000000 --- a/modules/commands.lua +++ /dev/null @@ -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 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/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/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/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 0cca96f..0000000 --- a/modules/tankmode.lua +++ /dev/null @@ -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 diff --git a/modules/unknown.lua b/modules/unknown.lua deleted file mode 100644 index fdb70f5..0000000 --- a/modules/unknown.lua +++ /dev/null @@ -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