BNP = BNP or {} local _, optionsPlayerClass = UnitClass("player") local comboOptionsClass = (optionsPlayerClass == "ROGUE" or optionsPlayerClass == "DRUID") local function Round(value, step) return math.floor((value / step) + 0.5) * step end local sliderCount = 0 local function CreateSlider(parent, label, minValue, maxValue, step, y) sliderCount = sliderCount + 1 local slider = CreateFrame("Slider", "BNPOptionsSlider" .. sliderCount, parent, "OptionsSliderTemplate") slider:SetPoint("TOPLEFT", parent, "TOPLEFT", 28, y) slider:SetWidth(220) slider:SetHeight(16) slider:SetMinMaxValues(minValue, maxValue) slider:SetValueStep(step) getglobal(slider:GetName() .. "Low"):SetText(tostring(minValue)) getglobal(slider:GetName() .. "High"):SetText(tostring(maxValue)) getglobal(slider:GetName() .. "Text"):SetText(label) return slider end local function CreateCheck(parent, label, y, onclick, x) local check = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate") check:SetPoint("TOPLEFT", parent, "TOPLEFT", x or 22, y) check:SetWidth(24) check:SetHeight(24) local text = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal") text:SetPoint("LEFT", check, "RIGHT", 4, 0) text:SetText(label) check.BNPLabel = text check:SetScript("OnClick", onclick) return check end -- 1.12-safe emergency recorder ------------------------------------------------ -- -- Keep a compact recorder directly in this already proven options file. The -- full diagnostic module replaces BNP.OpenAuraRecorder later when it loads. -- If an old/private 1.12 client refuses that larger module, this fallback still -- provides the window and the ClassicAPI aura data we actually need. local emergency = BNP.emergencyAuraRecorder or {} BNP.emergencyAuraRecorder = emergency emergency.lines = emergency.lines or {} emergency.active = false local function EmergencyValue(value, limit) if value == nil then return "nil" end local text = tostring(value) text = string.gsub(text, "\r", "\\r") text = string.gsub(text, "\n", "\\n") text = string.gsub(text, "\t", "\\t") limit = limit or 240 if string.len(text) > limit then text = string.sub(text, 1, limit) .. "..." end return text end local function EmergencyAppend(text) text = EmergencyValue(text, 1300) if string.len(table.concat(emergency.lines, "\n")) + string.len(text) > 60000 then if not emergency.limitHit then table.insert(emergency.lines, "[REPORT LIMIT REACHED - recording stopped]") emergency.limitHit = true emergency.active = false end return end table.insert(emergency.lines, text) emergency.dirty = true end local function EmergencyFields(data) if type(data) ~= "table" then return EmergencyValue(data) end local fields = {} local key, value for key, value in pairs(data) do local kind = type(value) if kind ~= "table" and kind ~= "function" and kind ~= "userdata" then table.insert(fields, EmergencyValue(key, 80) .. "=" .. EmergencyValue(value, 180)) end end table.sort(fields) return table.concat(fields, " | ") end local function EmergencyGUID(unit) local exists, guid = UnitExists(unit) if exists and guid then return guid end if UnitGUID then local ok, value = pcall(UnitGUID, unit) if ok then return value end end return nil end local function EmergencyWantedAura(aura) if type(aura) ~= "table" then return true end local spellID = tonumber(aura.spellId or aura.spellID) local name = string.lower(tostring(aura.name or "")) if spellID == 17794 or spellID == 17797 or spellID == 17798 or spellID == 17799 or spellID == 17800 then return true end if name == "shadow vulnerability" or name == "schattenverwundbarkeit" then return true end local playerGUID = EmergencyGUID("player") if aura.sourceGUID and playerGUID then return aura.sourceGUID == playerGUID end if aura.sourceUnit then return aura.sourceUnit == "player" end return false end local function EmergencyIsShadowBolt(spellID, spellName) local numeric = tonumber(spellID) if numeric == 686 or numeric == 695 or numeric == 705 or numeric == 1088 or numeric == 1106 or numeric == 7641 or numeric == 11659 or numeric == 11660 or numeric == 11661 or numeric == 25307 then return true end local name = string.lower(tostring(spellName or "")) return name == "shadow bolt" or name == "schattenblitz" end local function EmergencyRefresh() if not emergency.edit then return end emergency.edit:SetText(table.concat(emergency.lines, "\n")) emergency.edit:SetHeight(math.max(300, table.getn(emergency.lines) * 16 + 30)) if emergency.status then if emergency.active then emergency.status:SetText("RECORDING | Target: " .. EmergencyValue(UnitName("target") or "none", 100)) emergency.status:SetTextColor(0.2, 1, 0.35) else emergency.status:SetText("STOPPED | " .. tostring(table.getn(emergency.lines)) .. " lines") emergency.status:SetTextColor(1, 0.82, 0) end end emergency.dirty = false end local function EmergencySnapshot(reason, force) if not UnitExists("target") then if force then EmergencyAppend("AURA_SNAPSHOT | reason=" .. reason .. " | no target") end return end local auraLines = {} local signatureParts = {} local count = 0 local classic = type(C_UnitAuras) == "table" and type(C_UnitAuras.GetDebuffDataByIndex) == "function" local i if classic then for i = 1, 64 do local ok, aura = pcall(C_UnitAuras.GetDebuffDataByIndex, "target", i) if not ok then EmergencyAppend("CLASSICAPI_ERROR | " .. EmergencyValue(aura, 800)) break end if not aura then break end if EmergencyWantedAura(aura) then count = count + 1 local fields = EmergencyFields(aura) table.insert(signatureParts, fields) table.insert(auraLines, " CLASSIC_AURA " .. tostring(i) .. " | " .. fields) end end else for i = 1, 64 do local ok, texture, stacks, dispelType, spellID, r5, r6, r7, r8, r9, r10 = pcall(UnitDebuff, "target", i) if not ok or not texture then break end count = count + 1 local fields = "spellId=" .. EmergencyValue(spellID) .. " | stacks=" .. EmergencyValue(stacks) .. " | type=" .. EmergencyValue(dispelType) .. " | texture=" .. EmergencyValue(texture, 160) .. " | r5=" .. EmergencyValue(r5) .. " | r6=" .. EmergencyValue(r6) .. " | r7=" .. EmergencyValue(r7) .. " | r8=" .. EmergencyValue(r8) .. " | r9=" .. EmergencyValue(r9) .. " | r10=" .. EmergencyValue(r10) table.insert(signatureParts, fields) table.insert(auraLines, " LEGACY_AURA " .. tostring(i) .. " | " .. fields) end end local signature = table.concat(signatureParts, "||") if emergency.baselineOnly then emergency.lastSignature = signature return end if not force and signature == emergency.lastSignature then return end emergency.lastSignature = signature EmergencyAppend( "[+" .. string.format("%.3f", GetTime() - (emergency.startedAt or GetTime())) .. "] AURA_SNAPSHOT" .. " | reason=" .. EmergencyValue(reason) .. " | guid=" .. EmergencyValue(EmergencyGUID("target")) .. " | source=" .. (classic and "ClassicAPI" or "UnitDebuff") .. " | count=" .. tostring(count) ) if count == 0 then EmergencyAppend(" AURA none") else for i = 1, table.getn(auraLines) do EmergencyAppend(auraLines[i]) end end end local function EmergencyUnregister() if emergency.eventFrame then pcall(emergency.eventFrame.UnregisterAllEvents, emergency.eventFrame) end end local function EmergencyStop(reason) if not emergency.active then EmergencyRefresh() return end EmergencySnapshot("final", true) emergency.active = false EmergencyUnregister() if emergency.frame then emergency.frame:SetScript("OnUpdate", nil) end EmergencyAppend("=== END | reason=" .. EmergencyValue(reason or "user") .. " ===") EmergencyRefresh() end local function EmergencyStart() EmergencyUnregister() emergency.lines = {} emergency.limitHit = false emergency.lastSignature = nil emergency.startedAt = GetTime() emergency.elapsed = 0 emergency.active = true if emergency.frame and emergency.onUpdate then emergency.frame:SetScript("OnUpdate", emergency.onUpdate) end EmergencyAppend("BNP_CLASSICAPI_AURA_REPORT v2") EmergencyAppend( "ClassicAPI | version=" .. EmergencyValue(CLASSIC_API_VERSION or "not detected") .. " | C_UnitAuras=" .. ((type(C_UnitAuras) == "table" and type(C_UnitAuras.GetDebuffDataByIndex) == "function") and "yes" or "no") .. " | CopyToClipboard=" .. (CopyToClipboard and "yes" or "no") ) EmergencyAppend("Aura filter | own auras + Shadow Vulnerability from any Warlock") EmergencyAppend("Target | name=" .. EmergencyValue(UnitName("target")) .. " | guid=" .. EmergencyValue(EmergencyGUID("target"))) EmergencyAppend("Test | proc aura, refresh it while active, then let it expire") EmergencyAppend("=== LIVE RECORDING ===") local events = { "UNIT_CASTEVENT", "UNIT_AURA", "PLAYER_TARGET_CHANGED", "UNIT_SPELLCAST_SENT", "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP", "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_START", "UNIT_SPELLCAST_CHANNEL_STOP", } local i for i = 1, table.getn(events) do pcall(emergency.eventFrame.RegisterEvent, emergency.eventFrame, events[i]) end -- Learn the target's existing auras without printing them. Only changes -- occurring after Start Recording belong in the diagnostic report. emergency.baselineOnly = true EmergencySnapshot("baseline", false) emergency.baselineOnly = false EmergencyRefresh() end local function EmergencyCreateWindow() if emergency.frame then return emergency.frame end local frame = CreateFrame("Frame", "BNPEmergencyAuraRecorderFrame", UIParent) frame:SetWidth(720) frame:SetHeight(520) frame:SetPoint("CENTER", UIParent, "CENTER", 0, 20) frame:SetFrameStrata("DIALOG") frame:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 11, right = 12, top = 12, bottom = 11 } }) frame:SetMovable(true) frame:EnableMouse(true) frame:RegisterForDrag("LeftButton") frame:SetScript("OnDragStart", function() this:StartMoving() end) frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) frame:SetScript("OnHide", function() EmergencyStop("window closed") end) emergency.frame = frame local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") title:SetPoint("TOP", frame, "TOP", 0, -18) title:SetText("Missing Spell / Aura Recorder") local status = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") status:SetPoint("TOPLEFT", frame, "TOPLEFT", 26, -50) status:SetWidth(660) status:SetJustifyH("LEFT") emergency.status = status local start = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") start:SetPoint("TOPLEFT", frame, "TOPLEFT", 26, -76) start:SetWidth(126) start:SetHeight(24) start:SetText("Start Recording") start:SetScript("OnClick", EmergencyStart) local stop = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") stop:SetPoint("LEFT", start, "RIGHT", 8, 0) stop:SetWidth(82) stop:SetHeight(24) stop:SetText("Stop") stop:SetScript("OnClick", function() EmergencyStop("user") end) local copy = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") copy:SetPoint("LEFT", stop, "RIGHT", 8, 0) copy:SetWidth(110) copy:SetHeight(24) copy:SetText(CopyToClipboard and "Copy Report" or "Select All") copy:SetScript("OnClick", function() EmergencyRefresh() local report = table.concat(emergency.lines, "\n") if CopyToClipboard and pcall(CopyToClipboard, report) then emergency.status:SetText("COPIED | Paste with Ctrl+V") emergency.status:SetTextColor(0.2, 1, 0.35) else emergency.edit:SetFocus() emergency.edit:HighlightText() emergency.status:SetText("SELECTED | Press Ctrl+C") end end) local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5) local scroll = CreateFrame("ScrollFrame", "BNPEmergencyAuraRecorderScroll", frame, "UIPanelScrollFrameTemplate") scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 26, -112) scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -48, 30) local edit = CreateFrame("EditBox", "BNPEmergencyAuraRecorderEdit", scroll) edit:SetWidth(640) edit:SetHeight(300) edit:SetMultiLine(true) edit:SetAutoFocus(false) edit:SetFontObject(ChatFontNormal) edit:SetMaxLetters(65000) edit:SetScript("OnEscapePressed", function() this:ClearFocus() end) scroll:SetScrollChild(edit) emergency.edit = edit emergency.eventFrame = CreateFrame("Frame", "BNPEmergencyAuraRecorderEvents") emergency.eventFrame:SetScript("OnEvent", function() if not emergency.active then return end if event == "UNIT_AURA" and arg1 ~= "target" then return end if event == "UNIT_CASTEVENT" and arg1 ~= EmergencyGUID("player") then return end EmergencyAppend( "[+" .. string.format("%.3f", GetTime() - emergency.startedAt) .. "] EVENT | " .. EmergencyValue(event) .. " | a1=" .. EmergencyValue(arg1) .. " | a2=" .. EmergencyValue(arg2) .. " | a3=" .. EmergencyValue(arg3) .. " | a4=" .. EmergencyValue(arg4) .. " | a5=" .. EmergencyValue(arg5) .. " | a6=" .. EmergencyValue(arg6) ) emergency.forceSnapshot = event == "PLAYER_TARGET_CHANGED" or (event == "UNIT_CASTEVENT" and EmergencyIsShadowBolt(arg4)) or (event == "UNIT_SPELLCAST_SENT" and EmergencyIsShadowBolt(arg4, arg5)) or (event ~= "UNIT_SPELLCAST_SENT" and string.find(tostring(event or ""), "UNIT_SPELLCAST_", 1, true) and EmergencyIsShadowBolt(arg3, arg4)) end) emergency.onUpdate = function() if not emergency.active then return end emergency.elapsed = (emergency.elapsed or 0) + arg1 if emergency.elapsed >= 0.12 then emergency.elapsed = 0 EmergencySnapshot(emergency.forceSnapshot and "event" or "poll", emergency.forceSnapshot) emergency.forceSnapshot = false if emergency.dirty then EmergencyRefresh() end end end return frame end -- This remains available even if the full recorder loads, so that module can -- fall back when a private client rejects one of the larger window calls. function BNP:OpenEmergencyAuraRecorder() local frame = EmergencyCreateWindow() frame:Show() EmergencyRefresh() end function BNP:StartEmergencyAuraRecorder() EmergencyStart() end -- This is intentionally defined before the large optional recorder module. -- A successful module load overwrites it; otherwise the button still works. function BNP:OpenAuraRecorder() self:OpenEmergencyAuraRecorder() end function BNP:CreateOptions() if self.optionsFrame then return end local frame = CreateFrame("Frame", "BNPOptionsFrame", UIParent) frame:SetWidth(310) frame:SetHeight(comboOptionsClass and 897 or 837) frame:SetPoint("CENTER", UIParent, "CENTER", 0, 80) frame:SetFrameStrata("DIALOG") frame:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 11, right = 12, top = 12, bottom = 11 } }) frame:SetMovable(true) frame:EnableMouse(true) frame:RegisterForDrag("LeftButton") frame:SetScript("OnDragStart", function() this:StartMoving() end) frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) frame:Hide() local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") title:SetPoint("TOP", frame, "TOP", 0, -18) title:SetText("Blizz Nameplates+") local version = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") version:SetPoint("TOP", title, "BOTTOM", 0, -4) version:SetText("Version " .. tostring(BNP.version or "?")) frame.versionText = version local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5) local function CreateSection(label, y) local text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") text:SetPoint("TOPLEFT", frame, "TOPLEFT", 22, y) text:SetText(label) local line = frame:CreateTexture(nil, "ARTWORK") line:SetTexture(1, 1, 1, 0.16) line:SetPoint("LEFT", text, "RIGHT", 8, 0) line:SetPoint("RIGHT", frame, "RIGHT", -22, 0) line:SetHeight(1) return text end local function SetCheckEnabled(check, enabled) if not check then return end if enabled then if check.Enable then check:Enable() end check:SetAlpha(1.0) if check.BNPLabel then check.BNPLabel:SetTextColor(1, 0.82, 0) end else if check.Disable then check:Disable() end check:SetAlpha(0.45) if check.BNPLabel then check.BNPLabel:SetTextColor(0.5, 0.5, 0.5) end end end -- NAMEPLATES ------------------------------------------------------------- CreateSection("Nameplates", -68) local scale = CreateSlider(frame, "Nameplate Scale", 0.70, 1.50, 0.05, -98) scale:SetScript("OnValueChanged", function() if not BNP_DB then return end local value = Round(this:GetValue(), 0.05) BNP_DB.nameplateScale = value getglobal(this:GetName() .. "Text"):SetText("Nameplate Scale: " .. string.format("%.2f", value)) if BNP.ApplyNameplateScaleAll then BNP:ApplyNameplateScaleAll() end end) frame.scaleSlider = scale local yOffset = CreateSlider(frame, "Nameplate Y Offset", 0, 50, 1, -140) yOffset:SetScript("OnValueChanged", function() if not BNP_DB then return end local value = math.floor(this:GetValue() + 0.5) BNP_DB.nameplateYOffset = value getglobal(this:GetName() .. "Text"):SetText("Nameplate Y Offset: +" .. value) if BNP.ApplyNameplateYOffsetAll then BNP:ApplyNameplateYOffsetAll() end end) frame.yOffsetSlider = yOffset local nonTargetAlpha = CreateSlider(frame, "Non-Target Alpha", 30, 100, 5, -182) nonTargetAlpha:SetScript("OnValueChanged", function() if not BNP_DB then return end local percent = math.floor((this:GetValue() / 5) + 0.5) * 5 if percent < 30 then percent = 30 end if percent > 100 then percent = 100 end BNP_DB.nonTargetAlpha = percent / 100 getglobal(this:GetName() .. "Text"):SetText("Non-Target Alpha: " .. percent .. "%") if BNP.RefreshNonTargetAlpha then BNP:RefreshNonTargetAlpha() end end) frame.nonTargetAlphaSlider = nonTargetAlpha local classColors = CreateCheck(frame, "Class Colors", -220, function() BNP_DB.classColors = this:GetChecked() and true or false if BNP_DB.classColors then local plate for plate in pairs(BNP.plates or {}) do plate.BNPClassColorApplied = nil plate.BNPClassR = nil plate.BNPClassG = nil plate.BNPClassB = nil end end end) frame.classColorsCheck = classColors local tank = CreateCheck(frame, "Tank Mode", -246, function() BNP_DB.tankMode = this:GetChecked() and true or false if BNP.UpdateTankMode then BNP:UpdateTankMode() end end) tank:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("Tank Mode", 1, 0.82, 0) GameTooltip:AddLine("Green: the unit is targeting you.", 1, 1, 1) GameTooltip:AddLine("Red: the unit is targeting someone else.", 1, 1, 1) GameTooltip:Show() end) tank:SetScript("OnLeave", function() GameTooltip:Hide() end) frame.tankCheck = tank local healthTextLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") healthTextLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, -280) healthTextLabel:SetText("Health Text") local healthTextDropdown = CreateFrame("Frame", "BNPHealthTextDropdown", frame, "UIDropDownMenuTemplate") healthTextDropdown:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -292) UIDropDownMenu_SetWidth(150, healthTextDropdown) local healthModeLabels = { off = "Off", percent = "Percent", hp = "HP", both = "HP + Percent", } local function SetHealthTextMode(mode) if not healthModeLabels[mode] then mode = "off" end BNP_DB.healthText = mode BNP_DB.healthPercent = (mode == "percent" or mode == "both") UIDropDownMenu_SetSelectedValue(healthTextDropdown, mode) UIDropDownMenu_SetText(healthModeLabels[mode], healthTextDropdown) if BNP.RefreshHealthPercent then BNP:RefreshHealthPercent() end end UIDropDownMenu_Initialize(healthTextDropdown, function() local modes = { "off", "percent", "hp", "both" } local i for i = 1, table.getn(modes) do local mode = modes[i] local info = {} info.text = healthModeLabels[mode] info.value = mode info.func = function() SetHealthTextMode(this.value) end info.checked = (BNP:GetHealthTextMode() == mode) UIDropDownMenu_AddButton(info) end end) frame.healthTextDropdown = healthTextDropdown frame.SetHealthTextMode = SetHealthTextMode local targetFocus = CreateCheck(frame, "Target Glow", -336, function() BNP_DB.targetFocus = this:GetChecked() and true or false if BNP.RefreshTargetFocus then BNP:RefreshTargetFocus() end if frame.UpdateDependentControls then frame:UpdateDependentControls() end end) frame.targetFocusCheck = targetFocus local glowColorLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") glowColorLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", 42, -366) glowColorLabel:SetText("Glow Color") frame.glowColorLabel = glowColorLabel local glowColorDropdown = CreateFrame("Frame", "BNPTargetGlowColorDropdown", frame, "UIDropDownMenuTemplate") glowColorDropdown:SetPoint("TOPLEFT", frame, "TOPLEFT", 24, -378) UIDropDownMenu_SetWidth(150, glowColorDropdown) local glowColorLabels = { white = "White", gold = "Gold", blue = "Blue", green = "Green", red = "Red", purple = "Purple", } local function SetTargetGlowColor(color) if not glowColorLabels[color] then color = "white" end BNP_DB.targetGlowColor = color UIDropDownMenu_SetSelectedValue(glowColorDropdown, color) UIDropDownMenu_SetText(glowColorLabels[color], glowColorDropdown) if BNP.RefreshTargetFocus then BNP:RefreshTargetFocus() end end UIDropDownMenu_Initialize(glowColorDropdown, function() local colors = { "white", "gold", "blue", "green", "red", "purple" } local i for i = 1, table.getn(colors) do local color = colors[i] local info = {} info.text = glowColorLabels[color] info.value = color info.func = function() SetTargetGlowColor(this.value) end info.checked = ((BNP_DB and BNP_DB.targetGlowColor) or "white") == color UIDropDownMenu_AddButton(info) end end) frame.glowColorDropdown = glowColorDropdown frame.SetTargetGlowColor = SetTargetGlowColor local targetOnTop = CreateCheck(frame, "Target Plate on Top", -422, function() BNP_DB.targetOnTop = this:GetChecked() and true or false if BNP.RefreshTargetOnTop then BNP:RefreshTargetOnTop() end end) frame.targetOnTopCheck = targetOnTop -- AURAS ------------------------------------------------------------------ CreateSection("Auras", -462) local icon = CreateSlider(frame, "Aura Icon Size", 12, 32, 1, -492) icon:SetScript("OnValueChanged", function() if not BNP_DB then return end local value = math.floor(this:GetValue() + 0.5) BNP_DB.iconSize = value getglobal(this:GetName() .. "Text"):SetText("Aura Icon Size: " .. value) if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end end) frame.iconSlider = icon local debuffs = CreateCheck(frame, "Debuffs", -530, function() BNP_DB.debuffs = this:GetChecked() and true or false if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end end) frame.debuffsCheck = debuffs local crowdControl = CreateCheck(frame, "Crowd Control", -556, function() BNP_DB.crowdControl = this:GetChecked() and true or false if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end if frame.UpdateDependentControls then frame:UpdateDependentControls() end end) frame.crowdControlCheck = crowdControl local separateCCRow = CreateCheck(frame, "Display CCs in Separate Row", -582, function() BNP_DB.separateCCRow = this:GetChecked() and true or false if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end end, 42) frame.separateCCRowCheck = separateCCRow local showOtherCCs = CreateCheck(frame, "Show CCs from Other Players", -608, function() BNP_DB.showOtherCCs = this:GetChecked() and true or false if BNP.RefreshDebuffVisibility then BNP:RefreshDebuffVisibility() end end, 42) frame.showOtherCCsCheck = showOtherCCs -- CASTBAR ---------------------------------------------------------------- CreateSection("Castbar", -648) local castbars = CreateCheck(frame, "Castbars", -674, function() local enabled = this:GetChecked() and true or false if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(enabled) else BNP_DB.castbars = enabled end end) frame.castbarsCheck = castbars local castbarHeight = CreateSlider(frame, "Castbar Height", 4, 14, 1, -710) castbarHeight:SetScript("OnValueChanged", function() if not BNP_DB then return end local value = math.floor(this:GetValue() + 0.5) BNP_DB.castbarHeight = value getglobal(this:GetName() .. "Text"):SetText("Castbar Height: " .. value) if BNP.RefreshCastbarHeights then BNP:RefreshCastbarHeights() end end) frame.castbarHeightSlider = castbarHeight -- CLASS FEATURES --------------------------------------------------------- local noteY = -764 if comboOptionsClass then local classSection = CreateSection("Class Features", -750) frame.classSection = classSection local comboPoints = CreateCheck(frame, "Combo Points", -776, function() BNP_DB.comboPoints = this:GetChecked() and true or false if BNP.RefreshComboPoints then BNP:RefreshComboPoints() end if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end end) frame.comboPointsCheck = comboPoints noteY = -816 end local auraRecorder = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") auraRecorder:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, noteY) auraRecorder:SetWidth(254) auraRecorder:SetHeight(24) auraRecorder:SetText("Missing Spell / Aura...") auraRecorder:SetScript("OnClick", function() BNP:OpenAuraRecorder() end) auraRecorder:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("Missing Spell / Aura Recorder", 1, 0.82, 0) GameTooltip:AddLine("Records target aura IDs, SuperWoW events and refresh signals in one copyable report.", 1, 1, 1, true) GameTooltip:Show() end) auraRecorder:SetScript("OnLeave", function() GameTooltip:Hide() end) frame.auraRecorderButton = auraRecorder local note = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") note:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, noteY - 32) note:SetWidth(250) note:SetJustifyH("LEFT") note:SetText("Changes are applied immediately. Recorder output can be copied as text.") function frame:UpdateDependentControls() local ccEnabled = BNP:AreCrowdControlEnabled() SetCheckEnabled(self.separateCCRowCheck, ccEnabled) SetCheckEnabled(self.showOtherCCsCheck, ccEnabled) local glowEnabled = BNP:IsTargetFocusEnabled() if self.glowColorLabel then self.glowColorLabel:SetTextColor(glowEnabled and 1 or 0.5, glowEnabled and 0.82 or 0.5, glowEnabled and 0 or 0.5) end if self.glowColorDropdown then self.glowColorDropdown:SetAlpha(glowEnabled and 1.0 or 0.45) if glowEnabled then if UIDropDownMenu_EnableDropDown then UIDropDownMenu_EnableDropDown(self.glowColorDropdown) end else if UIDropDownMenu_DisableDropDown then UIDropDownMenu_DisableDropDown(self.glowColorDropdown) end end end end self.optionsFrame = frame end function BNP:SyncOptions() self:CreateOptions() local frame = self.optionsFrame frame.scaleSlider:SetValue(self:GetNameplateScale()) if frame.yOffsetSlider then local yOffset = self:GetNameplateYOffset() frame.yOffsetSlider:SetValue(yOffset) getglobal(frame.yOffsetSlider:GetName() .. "Text"):SetText("Nameplate Y Offset: +" .. yOffset) end if frame.nonTargetAlphaSlider then local alphaPercent = math.floor((self:GetNonTargetAlpha() * 100) + 0.5) frame.nonTargetAlphaSlider:SetValue(alphaPercent) getglobal(frame.nonTargetAlphaSlider:GetName() .. "Text"):SetText("Non-Target Alpha: " .. alphaPercent .. "%") end frame.iconSlider:SetValue(self:GetIconSize()) frame.castbarHeightSlider:SetValue(self:GetCastbarHeight()) frame.classColorsCheck:SetChecked(self:AreClassColorsEnabled()) if frame.debuffsCheck then frame.debuffsCheck:SetChecked(self:AreDebuffsEnabled()) end 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 frame.castbarsCheck:SetChecked(self:AreCastbarsEnabled()) frame.tankCheck:SetChecked(self:IsTankModeEnabled()) if frame.comboPointsCheck and comboOptionsClass then frame.comboPointsCheck:SetChecked(self:AreComboPointsEnabled()) end if frame.healthTextDropdown then local mode = self:GetHealthTextMode() UIDropDownMenu_SetSelectedValue(frame.healthTextDropdown, mode) local labels = { off = "Off", percent = "Percent", hp = "HP", both = "HP + Percent" } UIDropDownMenu_SetText(labels[mode] or "Off", frame.healthTextDropdown) end frame.targetFocusCheck:SetChecked(self:IsTargetFocusEnabled()) if frame.glowColorDropdown then local _, _, _, color = self:GetTargetGlowColor() local labels = { white = "White", gold = "Gold", blue = "Blue", green = "Green", red = "Red", purple = "Purple", } UIDropDownMenu_SetSelectedValue(frame.glowColorDropdown, color or "white") UIDropDownMenu_SetText(labels[color] or "White", frame.glowColorDropdown) end frame.targetOnTopCheck:SetChecked(self:IsTargetOnTopEnabled()) if frame.UpdateDependentControls then frame:UpdateDependentControls() end frame.versionText:SetText("Version " .. tostring(self.version or "?")) end function BNP:ToggleOptions() self:SyncOptions() if self.optionsFrame:IsShown() then self.optionsFrame:Hide() else self.optionsFrame:Show() end end function BNP:CreateMinimapButton() if self.minimapButton then return end BNP_DB = BNP_DB or {} if BNP_DB.minimapAngle == nil then BNP_DB.minimapAngle = 225 end local button = CreateFrame("Button", "BNPMinimapButton", Minimap) button:SetWidth(31) button:SetHeight(31) button:SetFrameStrata("MEDIUM") button:SetFrameLevel((Minimap:GetFrameLevel() or 1) + 8) button:RegisterForClicks("LeftButtonUp") button:RegisterForDrag("LeftButton") button:EnableMouse(true) button:SetMovable(true) -- Classic round minimap-button background. local bg = button:CreateTexture(nil, "BACKGROUND") bg:SetTexture("Interface\\Minimap\\UI-Minimap-Background") bg:SetWidth(22) bg:SetHeight(22) bg:SetPoint("CENTER", button, "CENTER", 0, 0) button.bg = bg -- Addon identity: simple BN+ lettering instead of a spell/item icon. -- Rendered directly by the client, so no external texture file is needed. local iconBG = button:CreateTexture(nil, "ARTWORK") iconBG:SetTexture(0.03, 0.03, 0.03, 1) iconBG:SetWidth(20) iconBG:SetHeight(20) iconBG:SetPoint("CENTER", button, "CENTER", 0, 0) button.iconBG = iconBG local label = button:CreateFontString(nil, "OVERLAY") label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") label:SetPoint("CENTER", button, "CENTER", 0, 0) label:SetText("BN+") label:SetTextColor(0.20, 0.60, 1.00) button.label = label -- Standard Blizzard circular minimap border. local border = button:CreateTexture(nil, "OVERLAY") border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder") border:SetWidth(54) border:SetHeight(54) border:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0) button.border = border local highlight = button:CreateTexture(nil, "HIGHLIGHT") highlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight") highlight:SetBlendMode("ADD") highlight:SetAllPoints(button) button.highlight = highlight local function UpdatePosition() local angle = tonumber(BNP_DB.minimapAngle) or 225 local radius = 78 local rad = math.rad(angle) button:ClearAllPoints() button:SetPoint( "CENTER", Minimap, "CENTER", math.cos(rad) * radius, math.sin(rad) * radius ) end local function UpdateAngleFromCursor() local mx, my = Minimap:GetCenter() local scale = 1 if UIParent.GetEffectiveScale then scale = UIParent:GetEffectiveScale() or 1 elseif UIParent.GetScale then scale = UIParent:GetScale() or 1 end local cx, cy = GetCursorPosition() cx = cx / scale cy = cy / scale local dx = cx - mx local dy = cy - my local angle if math.atan2 then angle = math.deg(math.atan2(dy, dx)) else if dx == 0 then angle = dy >= 0 and 90 or -90 else angle = math.deg(math.atan(dy / dx)) if dx < 0 then angle = angle + 180 end end end BNP_DB.minimapAngle = angle UpdatePosition() end button:SetScript("OnDragStart", function() this:SetScript("OnUpdate", UpdateAngleFromCursor) end) button:SetScript("OnDragStop", function() this:SetScript("OnUpdate", nil) UpdateAngleFromCursor() end) button:SetScript("OnClick", function() BNP:ToggleOptions() end) button:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_LEFT") GameTooltip:SetText("Blizz Nameplates+") GameTooltip:AddLine("Left-click: Open settings", 1, 1, 1) GameTooltip:AddLine("Drag: Move minimap button", 0.8, 0.8, 0.8) GameTooltip:Show() end) button:SetScript("OnLeave", function() GameTooltip:Hide() end) UpdatePosition() self.minimapButton = button end