diff --git a/BlizzNameplatesPlus.toc b/BlizzNameplatesPlus.toc index d687e15..14d8d4f 100644 --- a/BlizzNameplatesPlus.toc +++ b/BlizzNameplatesPlus.toc @@ -22,6 +22,7 @@ spells\mage.lua spells\druid.lua spells\shaman.lua spells\paladin.lua +spells\other_debuffs.lua spells\registry_shadow.lua modules\debug.lua modules\spellprobe.lua @@ -32,6 +33,7 @@ modules\fullalpha.lua modules\shagu_darkmode.lua modules\visualguard.lua modules\unknown.lua +modules\otherdebuffs.lua modules\auras.lua modules\immunities.lua modules\totems.lua diff --git a/core/config.lua b/core/config.lua index 3ec97d2..a2db030 100644 --- a/core/config.lua +++ b/core/config.lua @@ -35,6 +35,8 @@ BNP.defaults = BNP.defaults or { crowdControl = true, separateCCRow = true, showOtherCCs = true, + otherDebuffs = false, + otherDebuffSelections = {}, pvpImmunities = true, immunityIconSize = 18, immunityPosition = "top", @@ -50,6 +52,7 @@ BNP.defaults = BNP.defaults or { healthTextFontSize = 9, healthTextOutline = "outline", targetFocus = true, + targetOnlyNameplates = false, targetBorderColorEnabled = false, targetBorderColor = { r = 1.00, g = 0.20, b = 0.20 }, targetBorderBold = false, @@ -140,6 +143,8 @@ function BNP:InitConfig() if BNP_DB.crowdControl == nil then BNP_DB.crowdControl = (BNP_DB.debuffs ~= false) and true or false end if BNP_DB.separateCCRow == nil then BNP_DB.separateCCRow = self.defaults.separateCCRow end if BNP_DB.showOtherCCs == nil then BNP_DB.showOtherCCs = self.defaults.showOtherCCs end + if BNP_DB.otherDebuffs == nil then BNP_DB.otherDebuffs = self.defaults.otherDebuffs end + if type(BNP_DB.otherDebuffSelections) ~= "table" then BNP_DB.otherDebuffSelections = {} end if BNP_DB.pvpImmunities == nil then BNP_DB.pvpImmunities = self.defaults.pvpImmunities end if BNP_DB.immunityIconSize == nil then BNP_DB.immunityIconSize = BNP_DB.iconSize or self.defaults.immunityIconSize end if BNP_DB.immunityPosition == nil then BNP_DB.immunityPosition = self.defaults.immunityPosition end @@ -164,6 +169,7 @@ function BNP:InitConfig() if BNP_DB.healthTextFontSize == nil then BNP_DB.healthTextFontSize = self.defaults.healthTextFontSize end if BNP_DB.healthTextOutline == nil then BNP_DB.healthTextOutline = self.defaults.healthTextOutline end if BNP_DB.targetFocus == nil then BNP_DB.targetFocus = self.defaults.targetFocus end + if BNP_DB.targetOnlyNameplates == nil then BNP_DB.targetOnlyNameplates = self.defaults.targetOnlyNameplates end if BNP_DB.targetBorderColorEnabled == nil then BNP_DB.targetBorderColorEnabled = self.defaults.targetBorderColorEnabled end if BNP_DB.targetBorderBold == nil then BNP_DB.targetBorderBold = self.defaults.targetBorderBold end if type(BNP_DB.targetBorderColor) ~= "table" then @@ -526,6 +532,10 @@ function BNP:IsTargetFocusEnabled() return not BNP_DB or BNP_DB.targetFocus ~= false end +function BNP:IsTargetOnlyNameplatesEnabled() + return BNP_DB and BNP_DB.targetOnlyNameplates == true +end + function BNP:IsTargetScaleEnabled() return BNP_DB and BNP_DB.targetScaleEnabled == true end @@ -728,8 +738,51 @@ function BNP:IsAuraCooldownSpiralEnabled() return BNP_DB and BNP_DB.cooldownSpiral == true end +function BNP:AreOtherDebuffsEnabled() + return BNP_DB and BNP_DB.otherDebuffs == true +end + +function BNP:IsOtherDebuffSelected(key) + return key and self.OtherDebuffByKey and self.OtherDebuffByKey[key] + and BNP_DB and type(BNP_DB.otherDebuffSelections) == "table" + and BNP_DB.otherDebuffSelections[key] == true +end + +function BNP:HasSelectedOtherDebuffs() + if not BNP_DB or type(BNP_DB.otherDebuffSelections) ~= "table" then return false end + local key, enabled + for key, enabled in pairs(BNP_DB.otherDebuffSelections) do + if enabled == true and self.OtherDebuffByKey and self.OtherDebuffByKey[key] then return true end + end + return false +end + +function BNP:SetOtherDebuffSelected(key, enabled) + if not key then return end + BNP_DB = BNP_DB or {} + if type(BNP_DB.otherDebuffSelections) ~= "table" then BNP_DB.otherDebuffSelections = {} end + BNP_DB.otherDebuffSelections[key] = enabled and true or false +end + +function BNP:SetOtherDebuffClassEnabled(classKey, enabled) + local defs = self.OtherDebuffDefs and self.OtherDebuffDefs[classKey] + if not defs then return end + local i + for i = 1, table.getn(defs) do + self:SetOtherDebuffSelected(defs[i].key, enabled) + end +end + +function BNP:SetAllOtherDebuffsEnabled(enabled) + local classOrder = self.OtherDebuffClassOrder or {} + local i + for i = 1, table.getn(classOrder) do + self:SetOtherDebuffClassEnabled(classOrder[i], enabled) + end +end + function BNP:AreAnyAurasEnabled() - return self:AreDebuffsEnabled() or self:AreCrowdControlEnabled() + return self:AreDebuffsEnabled() or self:AreCrowdControlEnabled() or self:AreOtherDebuffsEnabled() end function BNP:IsSeparateCCRowEnabled() diff --git a/core/options.lua b/core/options.lua index 3324207..c05de23 100644 --- a/core/options.lua +++ b/core/options.lua @@ -605,11 +605,226 @@ function BNP:OpenAuraRecorder() self:OpenEmergencyAuraRecorder() end +local function BuildOtherDebuffsOptions(frame, otherPage, createSection) + +createSection(otherPage, "Other Class Debuffs", -4) + +local otherDebuffsEnabled = CreateCheck(otherPage, "Enable Other Class Debuffs", -28, function() + BNP_DB.otherDebuffs = this:GetChecked() and true or false + if BNP.RefreshOtherDebuffs then BNP:RefreshOtherDebuffs() end + if frame.RefreshOtherDebuffPage then frame:RefreshOtherDebuffPage() end +end, 22) +otherDebuffsEnabled:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("Other Class Debuffs", 1, 0.82, 0) + GameTooltip:AddLine("Shows selected non-CC debuffs applied by other players/classes. The feature is off by default and never changes BNP's own-cast tracking.", 1, 1, 1, true) + GameTooltip:Show() +end) +otherDebuffsEnabled:SetScript("OnLeave", function() GameTooltip:Hide() end) +frame.otherDebuffsEnabledCheck = otherDebuffsEnabled + +local otherNote = otherPage:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +otherNote:SetPoint("TOPLEFT", otherPage, "TOPLEFT", 28, -58) +otherNote:SetWidth(390) +otherNote:SetJustifyH("LEFT") +otherNote:SetText("Select which non-CC foreign debuffs BNP should display. Crowd Control stays in BNP's dedicated CC system.") + +local OTHER_TAB_W = 80 +local OTHER_TAB_H = 20 +local OTHER_TAB_GAP = 3 +local OTHER_TAB_X = 5 +local OTHER_TAB_Y = -90 +frame.otherDebuffClassTabs = {} +frame.otherSelectedClass = "warrior" + +local classOrder = BNP.OtherDebuffClassOrder or { "warrior", "rogue", "hunter", "mage", "warlock", "priest", "druid", "shaman", "paladin" } +local classLabels = BNP.OtherDebuffClassLabels or {} +local classColors = BNP.OtherDebuffClassColors or {} + +local i +for i = 1, table.getn(classOrder) do + local classKey = classOrder[i] + local tab = CreateFrame("Button", nil, otherPage) + tab:SetWidth(OTHER_TAB_W) + tab:SetHeight(OTHER_TAB_H) + tab:EnableMouse(true) + if i <= 5 then + tab:SetPoint("TOPLEFT", otherPage, "TOPLEFT", OTHER_TAB_X + (i - 1) * (OTHER_TAB_W + OTHER_TAB_GAP), OTHER_TAB_Y) + else + tab:SetPoint("TOPLEFT", otherPage, "TOPLEFT", OTHER_TAB_X + (i - 6) * (OTHER_TAB_W + OTHER_TAB_GAP), OTHER_TAB_Y - OTHER_TAB_H - 3) + end + + local color = classColors[classKey] or { 0.6, 0.6, 0.6 } + local bg = tab:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(tab) + bg:SetTexture(color[1] * 0.4, color[2] * 0.4, color[3] * 0.4, 0.85) + tab.BNPBG = bg + + local text = tab:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + text:SetPoint("CENTER", tab, "CENTER", 0, 0) + text:SetText(classLabels[classKey] or classKey) + text:SetTextColor(color[1], color[2], color[3]) + tab.BNPText = text + tab.BNPClassKey = classKey + + tab:SetScript("OnClick", function() + frame.otherSelectedClass = this.BNPClassKey + if frame.RefreshOtherDebuffPage then frame:RefreshOtherDebuffPage() end + end) + tab:SetScript("OnEnter", function() + local c = classColors[this.BNPClassKey] or { 0.6, 0.6, 0.6 } + if this.BNPBG then this.BNPBG:SetTexture(c[1] * 0.6, c[2] * 0.6, c[3] * 0.6, 1) end + end) + tab:SetScript("OnLeave", function() + local c = classColors[this.BNPClassKey] or { 0.6, 0.6, 0.6 } + local selected = frame.otherSelectedClass == this.BNPClassKey + if this.BNPBG then + if selected then this.BNPBG:SetTexture(c[1] * 0.55, c[2] * 0.55, c[3] * 0.55, 1) + else this.BNPBG:SetTexture(c[1] * 0.4, c[2] * 0.4, c[3] * 0.4, 0.85) end + end + end) + + frame.otherDebuffClassTabs[classKey] = tab +end + +local selectedClassLabel = otherPage:CreateFontString(nil, "OVERLAY", "GameFontNormal") +selectedClassLabel:SetPoint("TOPLEFT", otherPage, "TOPLEFT", 14, -147) +selectedClassLabel:SetText("Warrior") +frame.otherSelectedClassLabel = selectedClassLabel + +local enableClassButton = CreateFrame("Button", nil, otherPage, "UIPanelButtonTemplate") +enableClassButton:SetWidth(92) +enableClassButton:SetHeight(22) +enableClassButton:SetPoint("TOPRIGHT", otherPage, "TOPRIGHT", -112, -140) +enableClassButton:SetText("Enable All") +enableClassButton:SetScript("OnClick", function() + BNP:SetAllOtherDebuffsEnabled(true) + if BNP.RefreshOtherDebuffs then BNP:RefreshOtherDebuffs() end + if frame.RefreshOtherDebuffPage then frame:RefreshOtherDebuffPage() end +end) + +local disableClassButton = CreateFrame("Button", nil, otherPage, "UIPanelButtonTemplate") +disableClassButton:SetWidth(92) +disableClassButton:SetHeight(22) +disableClassButton:SetPoint("TOPRIGHT", otherPage, "TOPRIGHT", -14, -140) +disableClassButton:SetText("Disable All") +disableClassButton:SetScript("OnClick", function() + BNP:SetAllOtherDebuffsEnabled(false) + if BNP.RefreshOtherDebuffs then BNP:RefreshOtherDebuffs() end + if frame.RefreshOtherDebuffPage then frame:RefreshOtherDebuffPage() end +end) + +frame.otherEnableClassButton = enableClassButton +frame.otherDisableClassButton = disableClassButton +frame.otherDebuffRows = {} + +for i = 1, 12 do + local row = CreateFrame("Frame", nil, otherPage) + row:SetWidth(405) + row:SetHeight(28) + row:SetPoint("TOPLEFT", otherPage, "TOPLEFT", 14, -174 - ((i - 1) * 29)) + + local icon = row:CreateTexture(nil, "ARTWORK") + icon:SetWidth(20) + icon:SetHeight(20) + icon:SetPoint("LEFT", row, "LEFT", 2, 0) + icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + row.BNPIcon = icon + + local check = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate") + check:SetWidth(24) + check:SetHeight(24) + check:SetPoint("LEFT", icon, "RIGHT", 5, 0) + row.BNPCheck = check + + local label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + label:SetPoint("LEFT", check, "RIGHT", 3, 0) + label:SetWidth(300) + label:SetJustifyH("LEFT") + row.BNPLabel = label + + check:SetScript("OnClick", function() + if not this.BNPDebuffKey then return end + BNP:SetOtherDebuffSelected(this.BNPDebuffKey, this:GetChecked() and true or false) + if BNP.RefreshOtherDebuffs then BNP:RefreshOtherDebuffs() end + end) + check:SetScript("OnEnter", function() + local def = this.BNPDebuffDef + if not def then return end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(def.label or def.key, 1, 0.82, 0) + if def.description then GameTooltip:AddLine(def.description, 1, 1, 1, true) end + GameTooltip:Show() + end) + check:SetScript("OnLeave", function() GameTooltip:Hide() end) + + frame.otherDebuffRows[i] = row +end + +local emptyClassNote = otherPage:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +emptyClassNote:SetPoint("TOPLEFT", otherPage, "TOPLEFT", 20, -184) +emptyClassNote:SetWidth(380) +emptyClassNote:SetJustifyH("LEFT") +emptyClassNote:SetText("No shared Shaman debuffs are defined in the imported Cursive class list.") +emptyClassNote:Hide() +frame.otherEmptyClassNote = emptyClassNote + +function frame:RefreshOtherDebuffPage() + if self.otherDebuffsEnabledCheck then + self.otherDebuffsEnabledCheck:SetChecked(BNP.AreOtherDebuffsEnabled and BNP:AreOtherDebuffsEnabled() or false) + end + + local selected = self.otherSelectedClass or "warrior" + local defs = BNP.OtherDebuffDefs and BNP.OtherDebuffDefs[selected] or nil + local selectedColor = classColors[selected] or { 1, 0.82, 0 } + if self.otherSelectedClassLabel then + self.otherSelectedClassLabel:SetText(classLabels[selected] or selected) + self.otherSelectedClassLabel:SetTextColor(selectedColor[1], selectedColor[2], selectedColor[3]) + end + + local classKey, tab + for classKey, tab in pairs(self.otherDebuffClassTabs or {}) do + local c = classColors[classKey] or { 0.6, 0.6, 0.6 } + if tab.BNPBG then + if classKey == selected then tab.BNPBG:SetTexture(c[1] * 0.55, c[2] * 0.55, c[3] * 0.55, 1) + else tab.BNPBG:SetTexture(c[1] * 0.4, c[2] * 0.4, c[3] * 0.4, 0.85) end + end + end + + local count = defs and table.getn(defs) or 0 + if self.otherEmptyClassNote then + if count == 0 then self.otherEmptyClassNote:Show() else self.otherEmptyClassNote:Hide() end + end + + local rowIndex + for rowIndex = 1, table.getn(self.otherDebuffRows or {}) do + local row = self.otherDebuffRows[rowIndex] + local def = defs and defs[rowIndex] or nil + if def then + row.BNPDef = def + row.BNPCheck.BNPDebuffKey = def.key + row.BNPCheck.BNPDebuffDef = def + row.BNPCheck:SetChecked(BNP.IsOtherDebuffSelected and BNP:IsOtherDebuffSelected(def.key) or false) + row.BNPIcon:SetTexture(BNP.GetOtherDebuffTexture and BNP:GetOtherDebuffTexture(def) or "Interface\\Icons\\INV_Misc_QuestionMark") + row.BNPLabel:SetText(def.label or def.key) + row.BNPLabel:SetTextColor(selectedColor[1], selectedColor[2], selectedColor[3]) + row:Show() + else + row.BNPDef = nil + row.BNPCheck.BNPDebuffKey = nil + row.BNPCheck.BNPDebuffDef = nil + row:Hide() + end + end +end + +end + function BNP:CreateOptions() if self.optionsFrame then return end local frame = CreateFrame("Frame", "BNPOptionsFrame", UIParent) - frame:SetWidth(430) + frame:SetWidth(470) frame:SetHeight(710) -- Keep the top edge in roughly the same place and add the extra room -- downward. The Icons page uses the height for Totems, Raid Marks and @@ -684,17 +899,19 @@ function BNP:CreateOptions() totems = CreatePage(), castbar = CreatePage(), target = CreatePage(), + other = CreatePage(), tools = CreatePage(), } frame.tabs = {} local tabDefs = { - { key = "nameplates", label = "Nameplates", width = 76 }, - { key = "auras", label = "Auras", width = 52 }, - { key = "totems", label = "Icons", width = 58 }, - { key = "castbar", label = "Castbar", width = 60 }, - { key = "target", label = "Target", width = 56 }, - { key = "tools", label = "Tools", width = 50 }, + { key = "nameplates", label = "Nameplates", width = 72 }, + { key = "auras", label = "Auras", width = 50 }, + { key = "totems", label = "Icons", width = 52 }, + { key = "castbar", label = "Castbar", width = 58 }, + { key = "target", label = "Target", width = 54 }, + { key = "other", label = "Other Debuffs", width = 84 }, + { key = "tools", label = "Tools", width = 46 }, } function frame:ShowTab(key) @@ -718,9 +935,12 @@ function BNP:CreateOptions() end end self.selectedTab = key + if key == "other" and self.RefreshOtherDebuffPage then + self:RefreshOtherDebuffPage() + end end - local tabX = 28 + local tabX = 15 local i for i = 1, table.getn(tabDefs) do local def = tabDefs[i] @@ -1740,6 +1960,21 @@ function BNP:CreateOptions() thickArrows:SetPoint("TOPLEFT", targetPage, "TOPLEFT", 204, -220) frame.targetArrowThickCheck = thickArrows + local targetOnlyNameplates = CreateCheck(targetPage, "Only Show Target", -250, function() + BNP_DB.targetOnlyNameplates = this:GetChecked() and true or false + if BNP.RefreshTargetOnlyNameplates then BNP:RefreshTargetOnlyNameplates() end + if frame.UpdateDependentControls then frame:UpdateDependentControls() end + end) + targetOnlyNameplates:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("Only Show Target", 1, 0.82, 0) + GameTooltip:AddLine("While a target is selected, hides every other nameplate by making it fully transparent.", 1, 1, 1, true) + GameTooltip:AddLine("With no target selected, all nameplates stay visible so you can still select one. Disabling this option immediately restores normal visibility.", 0.8, 0.8, 0.8, true) + GameTooltip:Show() + end) + targetOnlyNameplates:SetScript("OnLeave", function() GameTooltip:Hide() end) + frame.targetOnlyNameplatesCheck = targetOnlyNameplates + -- Target border CreateSection(targetPage, "Target Border", -280) @@ -1803,6 +2038,9 @@ function BNP:CreateOptions() end) frame.targetScaleSlider = targetScale + -- OTHER DEBUFFS TAB ------------------------------------------------------ + BuildOtherDebuffsOptions(frame, frame.pages.other, CreateSection) + -- TOOLS TAB -------------------------------------------------------------- local toolsPage = frame.pages.tools CreateSection(toolsPage, "Tools", -4) @@ -1971,6 +2209,12 @@ function BNP:CreateOptions() end SetCheckEnabled(self.targetArrowThickCheck, arrowsEnabled) + local targetOnlyEnabled = BNP.IsTargetOnlyNameplatesEnabled and BNP:IsTargetOnlyNameplatesEnabled() or false + if self.nonTargetAlphaSlider then + self.nonTargetAlphaSlider:SetAlpha(targetOnlyEnabled and 0.45 or 1.0) + if self.nonTargetAlphaSlider.EnableMouse then self.nonTargetAlphaSlider:EnableMouse(not targetOnlyEnabled) end + end + local targetScaleEnabled = BNP.IsTargetScaleEnabled and BNP:IsTargetScaleEnabled() or false if self.targetScaleSlider then self.targetScaleSlider:SetAlpha(targetScaleEnabled and 1.0 or 0.45) @@ -2109,6 +2353,8 @@ function BNP:SyncOptions() if frame.crowdControlCheck then frame.crowdControlCheck:SetChecked(self:AreCrowdControlEnabled()) end if frame.separateCCRowCheck then frame.separateCCRowCheck:SetChecked(self:IsSeparateCCRowEnabled()) end if frame.showOtherCCsCheck then frame.showOtherCCsCheck:SetChecked(self:ShowOtherPlayersCCs()) end + if frame.otherDebuffsEnabledCheck then frame.otherDebuffsEnabledCheck:SetChecked(self:AreOtherDebuffsEnabled()) end + if frame.RefreshOtherDebuffPage then frame:RefreshOtherDebuffPage() end if frame.pvpImmunitiesCheck then frame.pvpImmunitiesCheck:SetChecked(self:ArePvPImmunitiesEnabled()) end if frame.immunityPositionDropdown then local position = self:GetImmunityPosition() @@ -2169,6 +2415,9 @@ function BNP:SyncOptions() UIDropDownMenu_SetText(labels[outline] or "Outline", frame.healthTextOutlineDropdown) end frame.targetFocusCheck:SetChecked(self:IsTargetFocusEnabled()) + if frame.targetOnlyNameplatesCheck then + frame.targetOnlyNameplatesCheck:SetChecked(self.IsTargetOnlyNameplatesEnabled and self:IsTargetOnlyNameplatesEnabled() or false) + end if frame.targetBorderColorCheck then frame.targetBorderColorCheck:SetChecked(self.IsTargetBorderColorEnabled and self:IsTargetBorderColorEnabled() or false) end diff --git a/modules/auras.lua b/modules/auras.lua index 1d3b635..2c3dbdf 100644 --- a/modules/auras.lua +++ b/modules/auras.lua @@ -367,6 +367,21 @@ end local function ApplyTargetAuraPriority(plate, container) if not container then return end + -- Aura/CC containers are siblings of the projected nameplate and therefore + -- do not inherit its alpha. In Target Only mode explicitly mirror the plate + -- alpha so hidden non-target plates cannot leave floating DoT/CC icons behind. + -- DesiredAlpha fails open while target identity is unresolved, so this also + -- stays flicker-safe during target switches and dense nameplate recycling. + if BNP.IsTargetOnlyNameplatesEnabled and BNP:IsTargetOnlyNameplatesEnabled() then + local alpha = 1 + if plate and plate.GetAlpha then + local ok, value = pcall(plate.GetAlpha, plate) + if ok and value ~= nil then alpha = value end + end + SetTargetPriorityAuraAlpha(container, alpha) + return + end + -- Flicker fix: aura opacity must not depend on transient target/nameplate -- resolution. In dense raid stacks ClassicAPI/SuperWoW can briefly resolve -- a projected plate differently, which previously made the whole debuff/CC @@ -977,6 +992,7 @@ local function ClearGUIDAuraState(guid) if BNP.svExpiryByGUID then BNP.svExpiryByGUID[guid] = nil end if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end if BNP.guidLiveCCs then BNP.guidLiveCCs[guid] = nil end + if BNP.guidOtherDebuffs then BNP.guidOtherDebuffs[guid] = nil end if BNP.pendingAuras then BNP.pendingAuras[guid] = nil end if BNP.pvpAuraProtection then BNP.pvpAuraProtection[guid] = nil end if BNP.darkHarvest and BNP.darkHarvest.guid == guid then BNP.darkHarvest = nil end @@ -998,6 +1014,7 @@ local function ClearHarmfulAuraStateOnly(guid) if BNP.svExpiryByGUID then BNP.svExpiryByGUID[guid] = nil end if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end if BNP.guidLiveCCs then BNP.guidLiveCCs[guid] = nil end + if BNP.guidOtherDebuffs then BNP.guidOtherDebuffs[guid] = nil end if BNP.pendingAuras then BNP.pendingAuras[guid] = nil end if BNP.pvpAuraProtection then BNP.pvpAuraProtection[guid] = nil end if BNP.darkHarvest and BNP.darkHarvest.guid == guid then BNP.darkHarvest = nil end @@ -2684,6 +2701,10 @@ local function SyncSharedAuras(unit, guid, now, snapshot, eventAuthoritative) end +-- Optional other-class debuff tracking lives in modules/otherdebuffs.lua. +-- auras.lua only consumes that cache for rendering and supplies live snapshots. + + -- 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. @@ -2744,7 +2765,7 @@ local function NeedsExactLiveRemoval(key) end local function IsCrowdControlDef(def) - return def and EXACT_LIVE_REMOVAL[def.key] and true or false + return def and (def.isCC or EXACT_LIVE_REMOVAL[def.key]) and true or false end local function GetEntryIconSize(entry, forceCC) @@ -3071,6 +3092,9 @@ local function UpdateCCRow(plate, guid, cache, now) end end + -- Other Debuffs intentionally excludes Crowd Control. Foreign CCs are + -- handled only by BNP's dedicated CC tracker below. + if WantsForeignCCs() then local live = guid and BNP.guidLiveCCs[guid] local key, aura @@ -3561,6 +3585,41 @@ local function UpdatePlate(plate, now) end end + -- Selected non-CC live debuffs from other players/classes are appended + -- after the local player's own entries. Crowd Control is handled exclusively + -- by BNP's dedicated CC tracker and never enters this list. + if BNP:WantsOtherDebuffs() and guid then + local otherLive = BNP.guidOtherDebuffs and BNP.guidOtherDebuffs[guid] + local classOrder = BNP.OtherDebuffClassOrder or {} + local classIndex, classKey, defs, defIndex, otherDef, otherAura + for classIndex = 1, table.getn(classOrder) do + if activeCount >= MAX_VISIBLE_ICONS then break end + classKey = classOrder[classIndex] + defs = BNP.OtherDebuffDefs and BNP.OtherDebuffDefs[classKey] or nil + if defs then + for defIndex = 1, table.getn(defs) do + if activeCount >= MAX_VISIBLE_ICONS then break end + otherDef = defs[defIndex] + if BNP:IsOtherDebuffDefSelected(otherDef) then + otherAura = otherLive and otherLive[otherDef.key] or nil + if otherAura and otherAura.livePresence + and not BNP:ActiveAuraHasSpellID(active, activeCount, otherAura.spellID) then + activeCount = activeCount + 1 + local entry = activePool[activeCount] + active[activeCount] = entry + entry.def = otherDef + entry.aura = otherAura + local otherRemaining = otherAura.expires and (otherAura.expires - now) or nil + if otherRemaining and otherRemaining <= 0 then otherRemaining = nil end + entry.remaining = otherRemaining + entry.svRenderRevision = nil + end + end + end + end + end + end + if activeCount > 1 then table.sort(active, BNP._AuraEntryOrderLess) end @@ -3725,6 +3784,7 @@ function BNP:_HandleClassicUnitAura(token, plate) -- this exact event snapshot. eventAuthoritative=true removes the old -- multi-miss protection that was only required for projected SuperWoW units. SyncSharedAuras(token, guid, now, snapshot, true) + BNP:SyncOtherDebuffs(token, guid, now, snapshot, true) if BNP.guidAuras[guid] then SyncAuraRemoval(token, guid, now, snapshot, GetTargetGUID(), true) end @@ -3900,11 +3960,12 @@ renderer:SetScript("OnUpdate", function() if doTargetRemovalScan and targetGUID and not targetSuppressed then local targetSnapshot = nil - if HAS_SHARED_AURAS or CacheHasTrackedAuras(BNP.guidAuras[targetGUID]) then + if HAS_SHARED_AURAS or (BNP.WantsOtherDebuffs and BNP:WantsOtherDebuffs()) or CacheHasTrackedAuras(BNP.guidAuras[targetGUID]) then targetSnapshot = CaptureLiveDebuffSnapshot("target", now) end SyncSharedAuras("target", targetGUID, now, targetSnapshot) + BNP:SyncOtherDebuffs("target", targetGUID, now, targetSnapshot) if BNP.guidAuras[targetGUID] then SyncAuraRemoval("target", targetGUID, now, targetSnapshot, targetGUID) @@ -3951,11 +4012,12 @@ renderer:SetScript("OnUpdate", function() local identityTrusted = stableGUID and stableGUID == guid if identityTrusted then local snapshot = nil - if HAS_SHARED_AURAS or CacheHasTrackedAuras(BNP.guidAuras[stableGUID]) then + if HAS_SHARED_AURAS or (BNP.WantsOtherDebuffs and BNP:WantsOtherDebuffs()) or CacheHasTrackedAuras(BNP.guidAuras[stableGUID]) then snapshot = CaptureLiveDebuffSnapshot(token, now) end SyncSharedAuras(token, stableGUID, now, snapshot) + BNP:SyncOtherDebuffs(token, stableGUID, now, snapshot) -- Non-target exact-removal also requires multiple misses below, so -- crowded raid aura lists cannot make Banish/CC blink. diff --git a/modules/fullalpha.lua b/modules/fullalpha.lua index c2e1979..975a329 100644 --- a/modules/fullalpha.lua +++ b/modules/fullalpha.lua @@ -271,8 +271,47 @@ local function IsForeignTagged(plate) end local function DesiredAlpha(plate, targetGUID) - -- No selected target: keep all nameplates fully visible. targetGUID = targetGUID or GetTargetGUID() + + -- Target Only is deliberately implemented with alpha instead of Hide()/Show(). + -- Blizzard owns projected nameplate visibility and recycles these frames as + -- units enter/leave the screen. Setting alpha to zero hides only the visual + -- while leaving the client free to manage the underlying frame safely. + -- + -- With no selected target, keep every plate visible so the player can still + -- click a nameplate to acquire a target. Once a target exists, Target Only + -- must be fail-closed: newly projected/recycled plates are hidden unless they + -- are positively identified as the current target. Failing open here caused + -- every newly shown plate to pop back in while rotating the camera because + -- the projected unit token can be unavailable for a few frames. + local targetOnly = BNP.IsTargetOnlyNameplatesEnabled and + BNP:IsTargetOnlyNameplatesEnabled() + if targetOnly then + if not targetGUID then return 1 end + + local strictTargetPlate = GetStrictTargetPlate(targetGUID) + if strictTargetPlate then + if plate == strictTargetPlate then return 1 end + return 0 + end + + -- Strict resolution can be briefly unavailable while Blizzard recycles a + -- projected plate. During that window only accept exact unit/GUID evidence; + -- never use the visual name/level fallback because identical mobs could + -- otherwise make several plates visible at once. + local token = GetPlateToken(plate) + if token and UnitIsUnit then + local ok, sameUnit = pcall(UnitIsUnit, token, "target") + if ok and sameUnit then return 1 end + end + + local plateGUID = GetVerifiedPlateGUID(plate) + if plateGUID and plateGUID == targetGUID then return 1 end + + return 0 + end + + -- No selected target: keep all nameplates fully visible. if not targetGUID then return 1 end local classicTargetPlate, classicTargetAvailable = @@ -826,15 +865,54 @@ local function ApplyTargetArrows(plate) end +local function ApplyTargetOnlyPresentation(plate, alpha, targetOnly) + if not plate then return end + + -- Auras/CC/immunities are sibling frames, so they do not inherit the + -- projected plate alpha. Keep them in lockstep with Target Only and restore + -- them to full opacity immediately when the option is disabled. + local detachedAlpha = targetOnly and alpha or 1 + local function SetObjectAlpha(object) + if object and object.SetAlpha and + (not object.GetAlpha or object:GetAlpha() ~= detachedAlpha) then + object:SetAlpha(detachedAlpha) + end + end + SetObjectAlpha(plate.BNPAuraContainer) + SetObjectAlpha(plate.BNPCCContainer) + SetObjectAlpha(plate.BNPImmunityContainer) + + -- BNP routes nameplate clicks through BNPScaleWrapper. Alpha-0 frames can + -- still receive mouse input in the WoW UI, so disable the wrapper hit area + -- for hidden non-targets and restore it for visible plates. + local wrapper = plate.BNPScaleWrapper + if wrapper and wrapper.EnableMouse then + local shouldEnableMouse = (not targetOnly) or alpha > 0 + wrapper:EnableMouse(shouldEnableMouse) + + if not shouldEnableMouse and BNP.BNPMouseoverOwner == wrapper then + BNP.BNPMouseoverOwner = nil + wrapper.BNPMouseoverUnit = nil + if SetMouseoverUnit then pcall(SetMouseoverUnit) end + if GameTooltip and GameTooltip.Hide then GameTooltip:Hide() end + end + end +end + local function ApplyTargetAlpha(plate) if not plate or not plate:IsShown() then return end local targetGUID = GetTargetGUID() local totemIconOnly = plate.BNPTotemLastKey and BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled() - local wanted = totemIconOnly and 1 or DesiredAlpha(plate, targetGUID) + local targetOnly = BNP.IsTargetOnlyNameplatesEnabled and + BNP:IsTargetOnlyNameplatesEnabled() + -- Totem icon replacement normally stays fully opaque, but Target Only must + -- apply to every projected nameplate consistently, including totems. + local wanted = (totemIconOnly and not targetOnly) and 1 or DesiredAlpha(plate, targetGUID) if plate:GetAlpha() ~= wanted then plate:SetAlpha(wanted) end + ApplyTargetOnlyPresentation(plate, wanted, targetOnly) -- Publish a STRICT single target identity for detached aura containers and -- target-only visuals. Do not reuse the alpha resolver here: its intentional @@ -901,7 +979,14 @@ if BNP.libnameplate then if not current then return end InvalidateTargetResolution() current.BNPAlphaIdentityHoldUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME - if current.GetAlpha and current.SetAlpha and current:GetAlpha() ~= 1 then + + -- Do not reset a recycled/newly projected plate to alpha 1 while Target + -- Only is active. That one-frame reset was visible as plates popping back + -- in when turning the camera. ApplyTargetAlpha below decides immediately + -- whether the frame is the target or must stay hidden. + local targetOnly = BNP.IsTargetOnlyNameplatesEnabled and + BNP:IsTargetOnlyNameplatesEnabled() + if not targetOnly and current.GetAlpha and current.SetAlpha and current:GetAlpha() ~= 1 then current:SetAlpha(1) end InstallAlphaGuard(current) @@ -924,8 +1009,12 @@ targetEvents:SetScript("OnEvent", function() local _, classicTargetAvailable = GetClassicAPITargetPlate(targetGUID) local holdUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME local plate + local targetOnly = BNP.IsTargetOnlyNameplatesEnabled and + BNP:IsTargetOnlyNameplatesEnabled() for plate in pairs(BNP.plates or {}) do - if classicTargetAvailable then + if classicTargetAvailable or targetOnly then + -- Target Only is intentionally fail-closed and therefore does not use + -- the legacy full-alpha identity hold during target switches. plate.BNPAlphaIdentityHoldUntil = nil ApplyTargetAlpha(plate) if BNP.MaintainTargetScale then BNP:MaintainTargetScale(plate) end @@ -963,3 +1052,15 @@ function BNP:RefreshNonTargetAlpha() ApplyTargetAlpha(plate) end end + +function BNP:RefreshTargetOnlyNameplates() + InvalidateTargetResolution() + local plate + for plate in pairs(self.plates or {}) do + ApplyTargetAlpha(plate) + end + -- Aura/CC rows are sibling frames so they do not inherit plate alpha. Make + -- them follow Target Only explicitly, then restore them immediately when the + -- option is disabled. + if self.RefreshAuraPriorityAlpha then self:RefreshAuraPriorityAlpha() end +end diff --git a/modules/names.lua b/modules/names.lua index ceebf0d..17001a6 100644 --- a/modules/names.lua +++ b/modules/names.lua @@ -30,6 +30,41 @@ local function EnsureCustomNameOverlay(plate) return overlay end +-- The custom name uses a separate FontString so Blizzard cannot recolor it. +-- A newly-created FontString does not automatically inherit the native +-- nameplate text shadow, so mirror that presentation here as well. +local function SyncCustomNameShadow(source, target) + if not source or not target then return end + + local shadowCopied = false + if source.GetShadowColor and target.SetShadowColor then + local r, g, b, a = source:GetShadowColor() + if r ~= nil and g ~= nil and b ~= nil then + target:SetShadowColor(r, g, b, a or 1) + shadowCopied = true + end + end + + -- Vanilla/older clients or replacement FontStrings may not expose the + -- shadow getters. Keep the classic Blizzard-style drop shadow in that case. + if not shadowCopied and target.SetShadowColor then + target:SetShadowColor(0, 0, 0, 1) + end + + local offsetCopied = false + if source.GetShadowOffset and target.SetShadowOffset then + local x, y = source:GetShadowOffset() + if x ~= nil and y ~= nil then + target:SetShadowOffset(x, y) + offsetCopied = true + end + end + + if not offsetCopied and target.SetShadowOffset then + target:SetShadowOffset(1, -1) + end +end + local function SyncCustomNameOverlay(plate) if not plate or not plate.name then return end @@ -64,6 +99,8 @@ local function SyncCustomNameOverlay(plate) if font and size then overlay:SetFont(font, size, flags) end end + SyncCustomNameShadow(plate.name, overlay) + if plate.name.GetText and overlay.SetText then overlay:SetText(plate.name:GetText() or "") end diff --git a/modules/otherdebuffs.lua b/modules/otherdebuffs.lua new file mode 100644 index 0000000..f9d4373 --- /dev/null +++ b/modules/otherdebuffs.lua @@ -0,0 +1,152 @@ +BNP = BNP or {} + +-- Optional live debuffs from other players/classes. +-- Kept outside auras.lua because that file is already close to Vanilla Lua's +-- per-chunk local-variable limit. This module owns only the foreign live cache; +-- BNP's proven own-cast tracker remains completely separate. + +BNP.guidOtherDebuffs = BNP.guidOtherDebuffs or {} + +local SEEN = {} +local seenGeneration = 0 + +function BNP:WantsOtherDebuffs() + return self.AreOtherDebuffsEnabled and self:AreOtherDebuffsEnabled() + and self.HasSelectedOtherDebuffs and self:HasSelectedOtherDebuffs() + and self.OtherDebuffByID ~= nil +end + +function BNP:IsOtherDebuffDefSelected(def) + return def and self.IsOtherDebuffSelected and self:IsOtherDebuffSelected(def.key) +end + +function BNP:SyncOtherDebuffs(unit, guid, now, snapshot, eventAuthoritative) + if not unit or not guid then return end + + if not self:WantsOtherDebuffs() then + self.guidOtherDebuffs[guid] = nil + return + end + + seenGeneration = seenGeneration + 1 + local generation = seenGeneration + local live = self.guidOtherDebuffs[guid] + local i + local maxIndex = snapshot and snapshot.count or 64 + + for i = 1, maxIndex do + local texture, stacks, auraSpellID, duration, expirationTime, source + if snapshot then + local entry = snapshot.entries[i] + if not entry then break end + texture = entry[1] + stacks = entry[2] + auraSpellID = entry[4] + duration = entry[5] + expirationTime = entry[6] + source = entry[7] + else + local dtype + texture, stacks, dtype, auraSpellID, duration, expirationTime, source = self:_ReadClassicDebuff(unit, i) + if not texture then break end + end + + local def = auraSpellID and self.OtherDebuffByID and self.OtherDebuffByID[auraSpellID] or nil + -- This feature is specifically for auras from other players/classes. When + -- ClassicAPI positively identifies our own source, let the proven own-cast + -- tracker remain the only owner of that icon/timer. + if def and self:IsOtherDebuffDefSelected(def) and not self:_IsOwnClassicAuraSource(source) then + SEEN[def.key] = generation + if not live then + live = {} + self.guidOtherDebuffs[guid] = live + end + + local aura = live[def.key] + if not aura then + aura = { + firstSeen = now, + order = def.order or (1000 + i), + } + live[def.key] = aura + end + + local previousStacks = tonumber(aura.stacks) or 0 + local currentStacks = tonumber(stacks) or 0 + aura.def = def + aura.spellID = auraSpellID + aura.texture = texture or (self.GetOtherDebuffTexture and self:GetOtherDebuffTexture(def)) or def.texture + aura.stacks = currentStacks + aura.lastSeen = now + aura.livePresence = true + aura.eventDriven = eventAuthoritative and true or false + aura.missingScans = nil + + local fallbackDuration = self.GetOtherDebuffDuration and self:GetOtherDebuffDuration(def, auraSpellID) or def.duration + local liveDuration = tonumber(duration) + local liveExpires = tonumber(expirationTime) + if liveExpires and liveExpires > now then + aura.duration = liveDuration and liveDuration > 0 and liveDuration or fallbackDuration + aura.expires = liveExpires + aura.estimated = nil + elseif not aura.expires then + aura.duration = fallbackDuration + if fallbackDuration and fallbackDuration > 0 then + aura.expires = now + fallbackDuration + aura.estimated = true + end + elseif currentStacks > previousStacks and fallbackDuration and fallbackDuration > 0 then + -- Stack-building raid debuffs commonly refresh their duration when a + -- new stack lands. Only use this estimate when ClassicAPI supplied no + -- authoritative expiration timestamp. + aura.duration = fallbackDuration + aura.expires = now + fallbackDuration + aura.estimated = true + end + end + end + + if live then + local key, aura + for key, aura in pairs(live) do + local def = aura and aura.def + if not def or not self:IsOtherDebuffDefSelected(def) then + live[key] = nil + elseif SEEN[key] ~= generation then + aura.missingScans = (aura.missingScans or 0) + 1 + if eventAuthoritative or (aura.missingScans >= 3 and now - (aura.lastSeen or 0) >= 0.40) then + live[key] = nil + end + else + aura.missingScans = nil + end + end + end +end + +function BNP:RefreshOtherDebuffs() + if not self:AreOtherDebuffsEnabled() then + self.guidOtherDebuffs = {} + else + local guid, live, key, aura + for guid, live in pairs(self.guidOtherDebuffs or {}) do + for key, aura in pairs(live or {}) do + if not aura or not aura.def or not self:IsOtherDebuffSelected(aura.def.key) then + live[key] = nil + end + end + end + end + + if self.RefreshDebuffVisibility then self:RefreshDebuffVisibility() end +end + +function BNP:ActiveAuraHasSpellID(active, count, spellID) + if not spellID then return false end + local i + for i = 1, count do + local entry = active[i] + if entry and entry.aura and entry.aura.spellID == spellID then return true end + end + return false +end diff --git a/modules/totems.lua b/modules/totems.lua index a4893e6..4d44b66 100644 --- a/modules/totems.lua +++ b/modules/totems.lua @@ -207,9 +207,15 @@ local function SuppressTotemPlateVisuals(plate, forceCapture) state.indicator = indicator end - -- Important PvP totems should remain clearly visible even when BNP's - -- non-target alpha option would normally fade the underlying nameplate. - if plate.SetAlpha and plate.GetAlpha and plate:GetAlpha() ~= 1 then plate:SetAlpha(1) end + -- Important PvP totems normally stay fully visible even when BNP's + -- non-target alpha option fades regular plates. Target Only is the explicit + -- exception: the totem icon is a child of the plate and must disappear with + -- every other non-target nameplate. + local targetOnly = BNP.IsTargetOnlyNameplatesEnabled and + BNP:IsTargetOnlyNameplatesEnabled() + if not targetOnly and plate.SetAlpha and plate.GetAlpha and plate:GetAlpha() ~= 1 then + plate:SetAlpha(1) + end if forceCapture or not state.nextCapture or now >= state.nextCapture then CaptureTotemPlateVisuals(plate, state, now) diff --git a/spells/other_debuffs.lua b/spells/other_debuffs.lua new file mode 100644 index 0000000..4a356fc --- /dev/null +++ b/spells/other_debuffs.lua @@ -0,0 +1,160 @@ +BNP = BNP or {} + +-- Optional non-CC debuffs from other players/classes. +-- The selectable list and grouping are based on the Cursive Raid class menu, +-- adapted to BNP's nameplate aura renderer. Crowd Control is intentionally +-- excluded here because BNP's dedicated CC tracker/menu already owns it. +-- These definitions remain separate from the local player's own-cast tracker. + +BNP.OtherDebuffClassOrder = { + "warrior", "rogue", "hunter", "mage", "warlock", + "priest", "druid", "shaman", "paladin", +} + +BNP.OtherDebuffClassLabels = { + warrior = "Warrior", + rogue = "Rogue", + hunter = "Hunter", + mage = "Mage", + warlock = "Warlock", + priest = "Priest", + druid = "Druid", + shaman = "Shaman", + paladin = "Paladin", +} + +BNP.OtherDebuffClassColors = { + warrior = { 0.78, 0.61, 0.43 }, + rogue = { 1.00, 0.96, 0.41 }, + hunter = { 0.67, 0.83, 0.45 }, + mage = { 0.41, 0.80, 0.94 }, + warlock = { 0.58, 0.51, 0.79 }, + priest = { 1.00, 1.00, 1.00 }, + druid = { 1.00, 0.49, 0.04 }, + shaman = { 0.00, 0.44, 0.87 }, + paladin = { 0.96, 0.55, 0.73 }, +} + +BNP.OtherDebuffDefs = { + warrior = { + { key="sunderarmor", label="Sunder Armor", category="armor", duration=30, maxStacks=5, + spellIDs={7386,7405,8380,11596,11597}, description="30s\nReduce 450 Armor up to 5 Times (2250)" }, + { key="demoshout", label="Demoralizing Shout", category="tank", duration=30, + spellIDs={1160,6190,11554,11555,11556}, description="30s\nReduces melee AP by 140" }, + { key="thunderclap", label="Thunder Clap", category="tank", duration=30, + spellIDs={6343,8198,8204,8205,11580,11581}, description="30s\nReduces attack speed by 10%" }, + { key="mortalstrike", label="Mortal Strike", category="healing", duration=10, + spellIDs={12294,21551,21552,21553}, description="10s\nReduces healing taken by 50%" }, + }, + rogue = { + { key="exposearmor", label="Expose Armor", category="armor", duration=30, + spellIDs={8647,8649,8650,11197,11198}, description="30s\nReduce 340 Armor per Combo Point (1700)" }, + { key="woundpoison", label="Wound Poison", category="healing", duration=15, maxStacks=5, + spellIDs={13218}, description="15s\nReduces healing taken by 5% up to 5 Times (25%)" }, + }, + hunter = { + { key="huntersmark", label="Hunter's Mark", category="utility", duration=120, + spellIDs={1130,14323,14324,14325}, description="120s\nIncreases Ranged AP by 110" }, + }, + mage = { + { key="firevulnerability", label="Fire Vulnerability", category="spellvuln", duration=30, maxStacks=5, + spellIDs={22959}, description="30s\nIncreases Fire Damage by 3% up to 5 Times (15%)" }, + { key="winterschill", label="Winter's Chill", category="spellvuln", duration=15, maxStacks=5, + spellIDs={12579}, description="15s\nIncreases Frost Crit Chance by 2% up to 5 Times (10%)" }, + { key="ignite", label="Ignite", category="personal", duration=4, + spellIDs={12654}, description="4s\n40% of crit damage over 2 ticks" }, + }, + warlock = { + { key="curseofrecklessness", label="Curse of Recklessness", category="armor", duration=120, + spellIDs={704,7658,7659,11717}, description="120s\nReduce Armor by 640" }, + { key="curseoftheelements", label="Curse of the Elements", category="spellvuln", duration=300, + spellIDs={1490,11721,11722}, description="300s\nReducing Fire and Frost Resistances by 75 and Increases Fire and Frost Damage by 10%" }, + { key="curseofshadow", label="Curse of Shadow", category="spellvuln", duration=300, + spellIDs={17862,17937}, description="300s\nReducing Shadow and Arcane Resistances by 75 and Increases Shadow and Arcane Damage by 10%" }, + { key="curseoftongues", label="Curse of Tongues", category="utility", duration=30, + spellIDs={1714,11719}, description="30s\nIncreases casting time by 50%" }, + { key="curseofweakness", label="Curse of Weakness", category="utility", duration=120, + spellIDs={702,1108,6205,7646,11707,11708}, description="120s\nReduces melee AP by 31" }, + { key="shadowvulnerability", label="Shadow Vulnerability", category="spellvuln", duration=10, + spellIDs={17794}, description="10s\nShadow Bolt and Drain Soul chance to Increases Shadow Damage by 20%" }, + }, + priest = { + { key="shadowweaving", label="Shadow Weaving", category="spellvuln", duration=15, maxStacks=5, + spellIDs={15258}, description="15s\nIncreases Shadow Damage by 3% up to 5 Times (15%)" }, + { key="burningzeal", label="Burning Zeal (Priest T3.5)", category="spellvuln", duration=18, + spellIDs={52980}, description="18s\n852 Holy damage over 18s, +2% Holy damage taken (T3.5 Set Proc)" }, + }, + druid = { + { key="faeriefire", label="Faerie Fire", category="armor", duration=40, + spellIDs={770,778,9749,9907,16855,17387,17388,17389,16857,17390,17391,17392}, description="40s\nReduce Armor by 505" }, + { key="demoroar", label="Demoralizing Roar", category="tank", duration=30, + spellIDs={99,1735,9490,9747,9898}, description="30s\nReduces melee AP by 108" }, + }, + shaman = { + -- Cursive's shared-debuff class list intentionally has no Shaman entries. + }, + paladin = { + { key="judgementoflight", label="Judgement of Light", category="utility", duration=10, + spellIDs={20185,20344,20345,20346}, description="10s\nMelee attacks heal attacker for 61" }, + { key="judgementofwisdom", label="Judgement of Wisdom", category="utility", duration=10, + spellIDs={20186,20354,20355}, description="10s\nAttacks restore 33 mana to attacker" }, + { key="judgementofthecrusader", label="Judgement of the Crusader", category="utility", duration=10, + spellIDs={21183,20188,20300,20301,20302,20303}, description="10s\nIncreases Holy damage taken by 140 and Increases melee and ranged AP" }, + }, +} + +BNP.OtherDebuffByID = BNP.OtherDebuffByID or {} +BNP.OtherDebuffByKey = BNP.OtherDebuffByKey or {} + +local classIndex, classKey, defs, i, def, j, spellID +for classIndex = 1, table.getn(BNP.OtherDebuffClassOrder) do + classKey = BNP.OtherDebuffClassOrder[classIndex] + defs = BNP.OtherDebuffDefs[classKey] or {} + for i = 1, table.getn(defs) do + def = defs[i] + def.class = classKey + def.otherDebuff = true + def.order = 1000 + (classIndex * 100) + i + BNP.OtherDebuffByKey[def.key] = def + for j = 1, table.getn(def.spellIDs or {}) do + spellID = def.spellIDs[j] + BNP.OtherDebuffByID[spellID] = def + end + end +end + +-- Remove selections from older test builds (notably CC entries) that are no +-- longer part of Other Debuffs. This also prevents stale keys from keeping the +-- optional foreign-debuff scanner active. +if BNP_DB and type(BNP_DB.otherDebuffSelections) == "table" then + local savedKey + for savedKey in pairs(BNP_DB.otherDebuffSelections) do + if not BNP.OtherDebuffByKey[savedKey] then + BNP_DB.otherDebuffSelections[savedKey] = nil + end + end +end + +function BNP:GetOtherDebuffDuration(def, spellID) + if not def then return nil end + if spellID and def.durations and def.durations[spellID] then + return def.durations[spellID] + end + return def.duration +end + +function BNP:GetOtherDebuffTexture(def) + if not def then return "Interface\\Icons\\INV_Misc_QuestionMark" end + if def.texture then return def.texture end + if SpellInfo and def.spellIDs then + local i + for i = 1, table.getn(def.spellIDs) do + local _, _, texture = SpellInfo(def.spellIDs[i]) + if texture then + def.texture = texture + return texture + end + end + end + return "Interface\\Icons\\INV_Misc_QuestionMark" +end