Add files via upload

This commit is contained in:
Relationship
2026-07-03 07:38:50 +01:00
committed by GitHub
parent 4975ad763d
commit 26c87f7b7c
8 changed files with 5003 additions and 2 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
## Notes: OctoWoW-compatible RelationshipsThreatPlates
## Author: Packaged By Relationship
## Version: 1.0
## SavedVariables: ThreatPlatesDB
## SavedVariables: RelationshipsThreatPlatesDB
## X-Website: https://github.com/Relationship-OctoWoW/RelationshipsThreatPlates
## X-GitHub: https://github.com/Relationship-OctoWoW/RelationshipsThreatPlates
ThreatPlates.xml
RelationshipsThreatPlates.xml
+8
View File
@@ -0,0 +1,8 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script file="RelationshipsThreatPlates_Core.lua"/>
<Script file="RelationshipsThreatPlates_Media.lua"/>
<Script file="RelationshipsThreatPlates_ThreatEngine.lua"/>
<Script file="RelationshipsThreatPlates_Threat.lua"/>
<Script file="RelationshipsThreatPlates_Nameplates.lua"/>
<Script file="RelationshipsThreatPlates_GUI.lua"/>
</Ui>
+177
View File
@@ -0,0 +1,177 @@
----------------------------------------------------------------------
-- RelationshipsThreatPlates_Core.lua
-- Core addon initialization, saved variables, slash commands
-- Compatible with WoW 1.12 (Vanilla / Turtle WoW / Octo WoW)
----------------------------------------------------------------------
-- Create the global addon object
RelationshipsThreatPlates = CreateFrame("Frame")
local addon = RelationshipsThreatPlates
addon.MAJOR = 1
addon.MINOR = 1
-- Version string
TP_VERSION = "2.3.1"
TP_ADDON_NAME = "RelationshipsThreatPlates"
-- ==================================================================
-- Default configuration
-- ==================================================================
local DEFAULT_CONFIG = {
enabled = true,
showText = true,
barHeight = 6,
barAlpha = 0.5,
barOffsetY = 1,
barOffsetX = -17.8,
barWidthScale = 0.8,
fontSize = 9,
fontOutline = true,
smoothColors = true,
showOnFriendly = false,
showOnNeutral = true,
showOnHostile = true,
showSpark = false,
showFillBG = true,
fillAlpha = 0.1,
glowEnabled = false,
glowSize = 3,
glowAsShadow = true,
guiPos = {
point = "CENTER",
relPoint = "CENTER",
x = 0,
y = 0,
},
}
-- ==================================================================
-- Safe UnitGUID wrapper - UnitGUID does not exist in Vanilla 1.12
-- Returns nil on vanilla clients, falls back to unit name
-- ==================================================================
function addon:SafeGUID(unit)
if UnitGUID then
local ok, guid = pcall(UnitGUID, unit)
if ok and guid then return guid end
end
return nil
end
-- ==================================================================
-- Get a unique identifier for a unit (GUID or name)
-- In vanilla 1.12, UnitGUID does not exist, so we use unit name
-- ==================================================================
function addon:GetUnitID(unit)
if not unit then return nil end
local guid = addon:SafeGUID(unit)
if guid then return guid end
local ok, name = pcall(UnitName, unit)
if ok and name then return name end
return nil
end
-- ==================================================================
-- Print helper
-- ==================================================================
function addon:ui_print(msg)
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cffbb99ffRelationshipsThreatPlates|r " .. tostring(msg))
end
end
-- ==================================================================
-- Deep copy utility
-- ==================================================================
local function DeepCopy(src)
if type(src) ~= "table" then return src end
local dst = {}
for k, v in pairs(src) do
if type(v) == "table" then
dst[k] = DeepCopy(v)
else
dst[k] = v
end
end
return dst
end
-- ==================================================================
-- Event handling
-- ==================================================================
addon:RegisterEvent("ADDON_LOADED")
addon:RegisterEvent("PLAYER_ENTERING_WORLD")
addon:SetScript("OnEvent", function()
if event == "ADDON_LOADED" and arg1 == TP_ADDON_NAME then
-- Initialize saved variables with defaults
if not RelationshipsThreatPlatesDB then
RelationshipsThreatPlatesDB = DeepCopy(DEFAULT_CONFIG)
else
-- Merge in any missing keys from defaults
for k, v in pairs(DEFAULT_CONFIG) do
if RelationshipsThreatPlatesDB[k] == nil then
RelationshipsThreatPlatesDB[k] = DeepCopy(v)
end
end
end
-- Ensure media textures
addon.Media.EnsureTextures()
-- Initialize modules
addon.ThreatEngine:Initialize()
addon.Threat:Initialize()
addon.Nameplates:Initialize()
addon.GUI:Initialize()
addon:ui_print("v" .. TP_VERSION .. " loaded! Type /tp for options.")
elseif event == "PLAYER_ENTERING_WORLD" then
-- Force a nameplate scan on entering world
if RelationshipsThreatPlatesDB and RelationshipsThreatPlatesDB.enabled then
addon.Nameplates:UpdateAll()
end
end
end)
-- ==================================================================
-- Slash commands
-- ==================================================================
SLASH_THREATPLATES1 = "/threatplates"
SLASH_THREATPLATES2 = "/tp"
SlashCmdList["THREATPLATES"] = function(msg)
-- Trim whitespace and lowercase (vanilla-safe)
msg = msg or ""
msg = string.lower(msg)
-- Trim leading/trailing spaces
msg = string.gsub(msg, "^%s*(.-)%s*$", "%1")
if msg == "reset" then
RelationshipsThreatPlatesDB = DeepCopy(DEFAULT_CONFIG)
addon:ui_print("Settings reset to defaults.")
addon.GUI:Refresh()
elseif msg == "toggle" then
RelationshipsThreatPlatesDB.enabled = not RelationshipsThreatPlatesDB.enabled
local state = RelationshipsThreatPlatesDB.enabled and "|cff33ff99ON|r" or "|cffff3333OFF|r"
addon:ui_print("Threat bars " .. state)
addon.Nameplates:UpdateAll()
elseif msg == "threat" then
addon.ThreatEngine:DumpThreat()
elseif msg == "help" then
addon:ui_print("Commands:")
DEFAULT_CHAT_FRAME:AddMessage(" |cffbb99ff/tp|r - Open GUI")
DEFAULT_CHAT_FRAME:AddMessage(" |cffbb99ff/tp toggle|r - Toggle threat bars on/off")
DEFAULT_CHAT_FRAME:AddMessage(" |cffbb99ff/tp reset|r - Reset to defaults")
DEFAULT_CHAT_FRAME:AddMessage(" |cffbb99ff/tp threat|r - Dump threat table (debug)")
DEFAULT_CHAT_FRAME:AddMessage(" |cffbb99ff/tp help|r - Show this help")
else
addon.GUI:Toggle()
end
end
-- ==================================================================
-- Expose DeepCopy on addon for other modules
-- ==================================================================
addon.DeepCopy = DeepCopy
+499
View File
@@ -0,0 +1,499 @@
----------------------------------------------------------------------
-- RelationshipsThreatPlates_GUI.lua
-- Configuration GUI panel with scrolling support
-- Styled after Kui Nameplates config: dark, minimal, clean
-- Standalone - no Ace3 or external config library required
-- Compatible with WoW 1.12 (Vanilla / Turtle WoW / Octo WoW)
----------------------------------------------------------------------
local addon = RelationshipsThreatPlates
addon.GUI = {}
-- GUI frame reference
local guiFrame = nil
-- Kui-style colour constants for GUI
local C = {
BG = { 0.10, 0.10, 0.10, 0.95 },
BORDER = { 0.30, 0.30, 0.30, 1.00 },
HEADER_BG = { 0.15, 0.15, 0.15, 1.00 },
HEADER_TEXT = { 0.75, 0.60, 1.00, 1.00 }, -- Kui purple
ACCENT = { 0.30, 0.70, 1.00, 1.00 }, -- Kui blue
TEXT = { 0.85, 0.85, 0.85, 1.00 },
TEXT_DIM = { 0.55, 0.55, 0.55, 1.00 },
BUTTON_BG = { 0.20, 0.20, 0.20, 1.00 },
BUTTON_HOVER = { 0.25, 0.25, 0.30, 1.00 },
BUTTON_TEXT = { 0.90, 0.90, 0.90, 1.00 },
CHECK_ON = { 0.30, 0.70, 1.00, 0.90 },
CHECK_OFF = { 0.30, 0.30, 0.30, 0.80 },
SLIDER_BG = { 0.15, 0.15, 0.15, 1.00 },
SLIDER_FILL = { 0.30, 0.70, 1.00, 0.80 },
DIVIDER = { 0.25, 0.25, 0.25, 0.60 },
SOLID = "Interface\\Buttons\\WHITE8x8",
}
-- Layout
local GUI_WIDTH = 320
local GUI_HEIGHT = 480
local HEADER_H = 30
local MARGIN = 14
local FONT_NORMAL = "Fonts\\FRIZQT__.TTF"
local FONT_SIZE = 10
-- Widget references for refresh
local widgets = {}
-- ==================================================================
-- Helper: safely set backdrop on a frame
-- ==================================================================
local function SafeSetBackdrop(frame, backdropTbl)
if frame.SetBackdrop then
frame:SetBackdrop(backdropTbl)
return true
end
return false
end
-- ==================================================================
-- Initialize GUI module
-- ==================================================================
function addon.GUI:Initialize()
-- Defer creation until first toggle
end
-- ==================================================================
-- Create the main GUI frame (Kui-style dark panel with scroll)
-- ==================================================================
function addon.GUI:CreateFrame()
if guiFrame then return end
-- ==============================================================
-- Main window frame
-- ==============================================================
local f = CreateFrame("Frame", "RelationshipsThreatPlatesGUI", UIParent)
f:SetWidth(GUI_WIDTH)
f:SetHeight(GUI_HEIGHT)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
SafeSetBackdrop(f, {
bgFile = C.SOLID,
edgeFile = C.SOLID,
tile = false, tileSize = 0,
edgeSize = 1,
})
f:SetBackdropColor(unpack(C.BG))
f:SetBackdropBorderColor(unpack(C.BORDER))
f:SetFrameStrata("DIALOG")
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
local _, _, point, _, _, x, y = this:GetPoint()
RelationshipsThreatPlatesDB.guiPos = { point = point, relPoint = point, x = x, y = y }
end)
f:Hide()
-- ==============================================================
-- Header bar (Kui purple accent)
-- ==============================================================
local header = CreateFrame("Frame", nil, f)
header:SetHeight(HEADER_H)
header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
SafeSetBackdrop(header, {
bgFile = C.SOLID,
edgeFile = C.SOLID,
tile = false, tileSize = 0,
edgeSize = 1,
})
header:SetBackdropColor(unpack(C.HEADER_BG))
header:SetBackdropBorderColor(unpack(C.BORDER))
header:EnableMouse(true)
header:RegisterForDrag("LeftButton")
header:SetScript("OnDragStart", function() f:StartMoving() end)
header:SetScript("OnDragStop", function()
f:StopMovingOrSizing()
local _, _, point, _, _, x, y = f:GetPoint()
RelationshipsThreatPlatesDB.guiPos = { point = point, relPoint = point, x = x, y = y }
end)
local headerText = header:CreateFontString(nil, "OVERLAY")
headerText:SetFont(FONT_NORMAL, 12, "OUTLINE")
headerText:SetPoint("CENTER", header, "CENTER", 0, 0)
headerText:SetTextColor(unpack(C.HEADER_TEXT))
headerText:SetText("RelationshipsThreatPlates")
-- Close button
local closeBtn = CreateFrame("Button", nil, header)
closeBtn:SetWidth(24)
closeBtn:SetHeight(20)
closeBtn:SetPoint("RIGHT", header, "RIGHT", -4, 0)
closeBtn:SetNormalTexture(C.SOLID)
closeBtn:GetNormalTexture():SetVertexColor(0.4, 0.4, 0.4, 0.8)
closeBtn:SetHighlightTexture(C.SOLID)
closeBtn:GetHighlightTexture():SetVertexColor(0.6, 0.6, 0.6, 0.8)
closeBtn:SetPushedTexture(C.SOLID)
closeBtn:GetPushedTexture():SetVertexColor(0.3, 0.3, 0.3, 0.8)
local closeText = closeBtn:CreateFontString(nil, "OVERLAY")
closeText:SetFont(FONT_NORMAL, 10, "OUTLINE")
closeText:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
closeText:SetTextColor(1, 1, 1, 1)
closeText:SetText("X")
closeBtn:SetFontString(closeText)
closeBtn:SetScript("OnClick", function()
addon.GUI:Hide()
end)
-- ==============================================================
-- Scroll frame - allows content to scroll when it overflows
-- ==============================================================
-- The scroll frame sits below the header and fills the rest of the window
local scrollFrame = CreateFrame("ScrollFrame", "RelationshipsThreatPlatesScrollFrame", f)
scrollFrame:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -HEADER_H)
scrollFrame:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -HEADER_H)
scrollFrame:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
scrollFrame:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
-- Scroll bar (vertical)
local scrollBar = CreateFrame("Slider", "RelationshipsThreatPlatesScrollBar", scrollFrame, "UIPanelScrollBarTemplate")
scrollBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -HEADER_H - 8)
scrollBar:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 8)
scrollBar:SetWidth(16)
scrollBar:SetMinMaxValues(0, 1000)
scrollBar:SetValueStep(20)
scrollBar:SetValue(0)
-- Style the scrollbar thumb and background to Kui look
local scrollName = scrollBar:GetName()
local scrollThumb = getglobal(scrollName .. "ThumbTexture")
if scrollThumb then
scrollThumb:SetTexture(C.SOLID)
scrollThumb:SetVertexColor(unpack(C.ACCENT))
end
-- Scroll child - the actual content frame that holds all settings
local scrollChild = CreateFrame("Frame", "RelationshipsThreatPlatesScrollChild", scrollFrame)
scrollChild:SetWidth(GUI_WIDTH - 30) -- leave room for scrollbar
-- Height will be set after building content based on total y offset
scrollFrame:SetScrollChild(scrollChild)
-- Connect scrollbar to scrollframe
scrollBar:SetScript("OnValueChanged", function()
scrollFrame:SetVerticalScroll(this:GetValue())
end)
-- Mouse wheel scrolling on the main frame
f:EnableMouseWheel(true)
f:SetScript("OnMouseWheel", function()
local delta = arg1
local current = scrollBar:GetValue()
local step = 40
if delta > 0 then
scrollBar:SetValue(math.max(0, current - step))
else
scrollBar:SetValue(math.min(1000, current + step))
end
end)
f.scrollFrame = scrollFrame
f.scrollBar = scrollBar
f.scrollChild = scrollChild
-- Build settings into the scroll child
widgets = {}
local totalHeight = self:BuildSettings(scrollChild)
-- Set the scroll child height to the actual content height
scrollChild:SetHeight(totalHeight + MARGIN)
-- Update scrollbar max based on content vs visible area
local visibleH = GUI_HEIGHT - HEADER_H
if totalHeight + MARGIN > visibleH then
scrollBar:SetMinMaxValues(0, totalHeight + MARGIN - visibleH)
else
scrollBar:SetMinMaxValues(0, 0)
scrollBar:Hide()
end
-- Restore saved position
if RelationshipsThreatPlatesDB and RelationshipsThreatPlatesDB.guiPos then
local pos = RelationshipsThreatPlatesDB.guiPos
if pos.point and pos.x and pos.y then
f:ClearAllPoints()
f:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y)
end
end
guiFrame = f
end
-- ==================================================================
-- Build all settings controls
-- Returns the total content height for scroll setup
-- ==================================================================
function addon.GUI:BuildSettings(parent)
local y = 0
local W = GUI_WIDTH - 30 -- account for scrollbar width
-- ==============================================================
-- Helper: Section label (Kui-style blue heading)
-- ==============================================================
local function SectionLabel(text)
local label = parent:CreateFontString(nil, "OVERLAY")
label:SetFont(FONT_NORMAL, FONT_SIZE + 1, "OUTLINE")
label:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y)
label:SetTextColor(unpack(C.ACCENT))
label:SetText(text)
y = y + 20
-- Divider line
local divider = parent:CreateTexture(nil, "ARTWORK")
divider:SetTexture(C.SOLID)
divider:SetVertexColor(unpack(C.DIVIDER))
divider:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y + 8)
divider:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -MARGIN, -y + 8)
divider:SetHeight(1)
y = y + 4
end
-- ==============================================================
-- Helper: Checkbox (custom frame-based)
-- ==============================================================
local function CheckBox(labelText, configKey, description)
local btn = CreateFrame("Frame", nil, parent)
btn:SetWidth(18)
btn:SetHeight(18)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y)
btn:EnableMouse(true)
SafeSetBackdrop(btn, {
bgFile = C.SOLID,
edgeFile = C.SOLID,
tile = false, tileSize = 0,
edgeSize = 1,
})
btn:SetBackdropColor(unpack(C.CHECK_OFF))
btn:SetBackdropBorderColor(unpack(C.BORDER))
local check = btn:CreateTexture(nil, "ARTWORK")
check:SetTexture(C.SOLID)
check:SetPoint("TOPLEFT", btn, "TOPLEFT", 3, -3)
check:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -3, 3)
check:SetVertexColor(unpack(C.CHECK_ON))
check:Hide()
btn.checkMark = check
btn.checked = false
if RelationshipsThreatPlatesDB[configKey] then
btn.checked = true
check:Show()
end
btn:SetScript("OnMouseDown", function()
this.checked = not this.checked
RelationshipsThreatPlatesDB[configKey] = this.checked
if this.checked then
this.checkMark:Show()
else
this.checkMark:Hide()
end
addon.Nameplates:RecreateAll()
end)
local label = parent:CreateFontString(nil, "OVERLAY")
label:SetFont(FONT_NORMAL, FONT_SIZE)
label:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN + 24, -y + 1)
label:SetTextColor(unpack(C.TEXT))
label:SetText(labelText)
y = y + 18
if description then
local desc = parent:CreateFontString(nil, "OVERLAY")
desc:SetFont(FONT_NORMAL, FONT_SIZE - 2)
desc:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN + 24, -y + 3)
desc:SetTextColor(unpack(C.TEXT_DIM))
desc:SetText(description)
y = y + 14
end
y = y + 4
widgets[configKey] = btn
end
-- ==============================================================
-- Helper: Slider (Kui-style flat slider)
-- ==============================================================
local function Slider(labelText, configKey, minVal, maxVal, step, suffix)
suffix = suffix or ""
local label = parent:CreateFontString(nil, "OVERLAY")
label:SetFont(FONT_NORMAL, FONT_SIZE)
label:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y)
label:SetTextColor(unpack(C.TEXT))
label:SetText(labelText)
y = y + 16
local slider = CreateFrame("Slider", "RelationshipsThreatPlatesSlider_" .. configKey, parent, "OptionsSliderTemplate")
slider:SetWidth(W - 60)
slider:SetHeight(16)
slider:SetPoint("TOPLEFT", parent, "TOPLEFT", MARGIN, -y)
slider:SetOrientation("HORIZONTAL")
slider:SetMinMaxValues(minVal, maxVal)
slider:SetValueStep(step)
slider:SetValue(RelationshipsThreatPlatesDB[configKey] or minVal)
local sliderName = slider:GetName()
local bgTexture = getglobal(sliderName .. "BG")
local fillTexture = getglobal(sliderName .. "Fill")
local thumbTexture = getglobal(sliderName .. "Thumb")
if bgTexture then
bgTexture:SetTexture(C.SOLID)
bgTexture:SetVertexColor(unpack(C.SLIDER_BG))
end
if fillTexture then
fillTexture:SetTexture(C.SOLID)
fillTexture:SetVertexColor(unpack(C.SLIDER_FILL))
end
if thumbTexture then
thumbTexture:SetTexture(C.SOLID)
thumbTexture:SetVertexColor(unpack(C.ACCENT))
end
local lowText = getglobal(sliderName .. "Low")
local highText = getglobal(sliderName .. "High")
if lowText then lowText:SetText("") end
if highText then highText:SetText("") end
local valText = parent:CreateFontString(nil, "OVERLAY")
valText:SetFont(FONT_NORMAL, FONT_SIZE - 1)
valText:SetPoint("LEFT", slider, "RIGHT", 6, 0)
valText:SetTextColor(unpack(C.ACCENT))
local curVal = RelationshipsThreatPlatesDB[configKey] or minVal
if step < 1 then
valText:SetText(string.format("%.1f", curVal) .. suffix)
else
valText:SetText(math.floor(curVal + 0.5) .. suffix)
end
slider:SetScript("OnValueChanged", function()
local val = this:GetValue()
val = math.floor(val / step + 0.5) * step
RelationshipsThreatPlatesDB[configKey] = val
if step < 1 then
valText:SetText(string.format("%.1f", val) .. suffix)
else
valText:SetText(math.floor(val + 0.5) .. suffix)
end
addon.Nameplates:RecreateAll()
end)
y = y + 24
widgets[configKey] = slider
end
-- ==============================================================
-- GENERAL SECTION
-- ==============================================================
SectionLabel("General")
CheckBox("Enable Threat Plates", "enabled", "Show threat bars above nameplates")
CheckBox("Show Threat Text", "showText", "Display percentage in the bar")
CheckBox("Smooth Colours", "smoothColors", "Gradient colour transition")
-- ==============================================================
-- APPEARANCE SECTION
-- ==============================================================
SectionLabel("Appearance")
Slider("Bar Height", "barHeight", 2, 20, 1, "px")
Slider("Bar Width Scale", "barWidthScale", 0.3, 1.0, 0.05, "")
Slider("Bar Offset Y", "barOffsetY", 0, 20, 1, "px")
Slider("Bar Offset X", "barOffsetX", -50, 50, 0.1, "px")
Slider("Bar Alpha", "barAlpha", 0.1, 1.0, 0.1, "")
Slider("Font Size", "fontSize", 6, 16, 1, "pt")
CheckBox("Font Outline", "fontOutline", "Outlined text for readability")
-- ==============================================================
-- KUI STYLE SECTION
-- ==============================================================
SectionLabel("Kui Style Effects")
CheckBox("Show Spark", "showSpark", "Bright edge on the bar's leading edge")
CheckBox("Show Fill Background", "showFillBG", "Dim ghost of bar colour behind the bar")
Slider("Fill Alpha", "fillAlpha", 0.05, 0.50, 0.05, "")
CheckBox("Enable Glow", "glowEnabled", "Border glow based on threat level")
CheckBox("Glow as Shadow", "glowAsShadow", "Subtle shadow instead of bright glow")
Slider("Glow Size", "glowSize", 1, 8, 1, "px")
-- ==============================================================
-- FILTER SECTION
-- ==============================================================
SectionLabel("Display Filters")
CheckBox("Show on Hostile", "showOnHostile", "Show threat bars on hostile units")
CheckBox("Show on Neutral", "showOnNeutral", "Show threat bars on neutral units")
CheckBox("Show on Friendly", "showOnFriendly", "Show threat bars on friendly units")
-- Return total content height for scroll setup
return y
end
-- ==================================================================
-- Toggle GUI visibility
-- ==================================================================
function addon.GUI:Toggle()
if not guiFrame then
self:CreateFrame()
end
if guiFrame:IsVisible() then
self:Hide()
else
self:Show()
end
end
-- ==================================================================
-- Show the GUI
-- ==================================================================
function addon.GUI:Show()
if not guiFrame then
self:CreateFrame()
end
guiFrame:Show()
end
-- ==================================================================
-- Hide the GUI
-- ==================================================================
function addon.GUI:Hide()
if guiFrame then
guiFrame:Hide()
end
end
-- ==================================================================
-- Refresh all GUI values from saved vars
-- ==================================================================
function addon.GUI:Refresh()
if not guiFrame then return end
for key, widget in pairs(widgets) do
-- Our custom checkbox (Frame with .checked field)
if widget.checked ~= nil then
local val = RelationshipsThreatPlatesDB[key]
widget.checked = val
if val then
widget.checkMark:Show()
else
widget.checkMark:Hide()
end
-- Slider
elseif widget.SetValue then
widget:SetValue(RelationshipsThreatPlatesDB[key])
end
end
end
+120
View File
@@ -0,0 +1,120 @@
----------------------------------------------------------------------
-- RelationshipsThreatPlates_Media.lua
-- Embedded media textures and fonts
-- Self-contained - generates textures procedurally, no external files
----------------------------------------------------------------------
local addon = RelationshipsThreatPlates -- Created in RelationshipsThreatPlates_Core.lua (loaded first via XML)
addon.Media = {}
-- ==================================================================
-- Procedural status bar texture (solid gradient)
-- Generated at runtime via texture coordinates on Blizzard's shared
-- gradient texture, matching Kui's clean flat bar style
-- ==================================================================
-- Use Blizzard's built-in solid textures as bar fill
-- These are always available and produce the clean flat look
addon.Media.BAR_TEXTURE = "Interface\\AddOns\\RelationshipsThreatPlates\\Media\\Bar"
addon.Media.BG_TEXTURE = "Interface\\AddOns\\RelationshipsThreatPlates\\Media\\BarBG"
addon.Media.SOLID_TEXTURE = "Interface\\Buttons\\WHITE8x8"
addon.Media.GLOW_TEXTURE = "Interface\\AddOns\\RelationshipsThreatPlates\\Media\\Glow"
addon.Media.SPARK_TEXTURE = "Interface\\AddOns\\RelationshipsThreatPlates\\Media\\Spark"
addon.Media.SHADOW_TEXTURE = "Interface\\AddOns\\RelationshipsThreatPlates\\Media\\ShadowBorder"
-- Font paths - use WoW's built-in fonts for zero-dependency
addon.Media.FONT_NORMAL = "Fonts\\FRIZQT__.TTF"
addon.Media.FONT_OUTLINE = "Fonts\\FRIZQT__.TTF"
-- Kui-style colour palette
addon.Media.COLOURS = {
-- Threat gradient: green -> yellow -> orange -> red
THREAT_LOW = { r = 0.33, g = 0.80, b = 0.33 },
THREAT_MEDIUM = { r = 1.00, g = 0.85, b = 0.00 },
THREAT_HIGH = { r = 1.00, g = 0.50, b = 0.00 },
THREAT_AGGRO = { r = 1.00, g = 0.10, b = 0.10 },
-- Kui-style background
BG_DARK = { r = 0.06, g = 0.06, b = 0.06, a = 0.90 },
-- Kui-style border glow colours
GLOW_TARGET = { r = 0.30, g = 0.70, b = 1.00, a = 0.60 },
GLOW_THREAT = { r = 0.90, g = 0.00, b = 0.00, a = 0.60 },
GLOW_SHADOW = { r = 0.00, g = 0.00, b = 0.00, a = 0.20 },
-- Text
TEXT_WHITE = { r = 1.00, g = 1.00, b = 1.00 },
TEXT_SHADOW = { r = 0.00, g = 0.00, b = 0.00, a = 1.00 },
-- GUI
GUI_BG = { r = 0.10, g = 0.10, b = 0.10, a = 0.95 },
GUI_BORDER = { r = 0.30, g = 0.30, b = 0.30, a = 1.00 },
GUI_HEADER = { r = 0.15, g = 0.15, b = 0.15, a = 1.00 },
GUI_ACCENT = { r = 0.30, g = 0.70, b = 1.00, a = 1.00 },
}
-- ==================================================================
-- Helper: brighten a colour by a factor (Kui-style)
-- ==================================================================
function addon.Media.Brighten(factor, r, g, b, a)
factor = factor or 0.3
if type(r) == "table" then
r, g, b, a = unpack(r)
end
a = a or 1
return math.min(1, r + (1 - r) * factor),
math.min(1, g + (1 - g) * factor),
math.min(1, b + (1 - b) * factor),
a
end
-- ==================================================================
-- Helper: get smooth interpolated threat colour
-- ==================================================================
function addon.Media.GetThreatColour(pct)
pct = tonumber(pct) or 0
pct = math.min(100, math.max(0, pct))
local c = addon.Media.COLOURS
local r, g, b
if pct < 50 then
local t = pct / 50
r = c.THREAT_LOW.r + (c.THREAT_MEDIUM.r - c.THREAT_LOW.r) * t
g = c.THREAT_LOW.g + (c.THREAT_MEDIUM.g - c.THREAT_LOW.g) * t
b = c.THREAT_LOW.b + (c.THREAT_MEDIUM.b - c.THREAT_LOW.b) * t
elseif pct < 80 then
local t = (pct - 50) / 30
r = c.THREAT_MEDIUM.r + (c.THREAT_HIGH.r - c.THREAT_MEDIUM.r) * t
g = c.THREAT_MEDIUM.g + (c.THREAT_HIGH.g - c.THREAT_MEDIUM.g) * t
b = c.THREAT_MEDIUM.b + (c.THREAT_HIGH.b - c.THREAT_MEDIUM.b) * t
else
local t = (pct - 80) / 20
t = math.min(1, math.max(0, t))
r = c.THREAT_HIGH.r + (c.THREAT_AGGRO.r - c.THREAT_HIGH.r) * t
g = c.THREAT_HIGH.g + (c.THREAT_AGGRO.g - c.THREAT_HIGH.g) * t
b = c.THREAT_HIGH.b + (c.THREAT_AGGRO.b - c.THREAT_HIGH.b) * t
end
return r, g, b
end
-- ==================================================================
-- Generate media textures as file data (using solid fallback)
-- These are created in the Media/ folder by the installer script
-- but we also provide runtime fallback generation
-- ==================================================================
function addon.Media.EnsureTextures()
-- Ensure the media directory textures exist
-- For standalone deployment, we use Blizzard built-in textures
-- with vertex colouring to achieve the Kui look
--
-- The bar texture is handled via WHITE8x8 with vertex colour
-- This gives the same clean flat look as Kui's procedural bar
addon.Media.BAR_TEXTURE = addon.Media.SOLID_TEXTURE
addon.Media.BG_TEXTURE = addon.Media.SOLID_TEXTURE
addon.Media.GLOW_TEXTURE = addon.Media.SOLID_TEXTURE
addon.Media.SPARK_TEXTURE = addon.Media.SOLID_TEXTURE
end
File diff suppressed because it is too large Load Diff
+828
View File
@@ -0,0 +1,828 @@
----------------------------------------------------------------------
-- RelationshipsThreatPlates_Threat.lua
-- Threat calculation bridge for WoW 1.12 (Vanilla API)
-- Works with Turtle WoW / Octo WoW threat hooks
-- Standalone - does not require Omen, KLHThreatMeter, or any lib
--
-- THREAT DATA SOURCES (in priority order):
-- 1. UnitDetailedThreatSituation (Turtle WoW server extension)
-- 2. GetThreatPercent API (custom server extension)
-- 3. UnitThreatSituation (partial API on some servers)
-- 4. ThreatEngine combat log parsing (precise per-1% values)
-- 5. Target-based heuristic (crude fallback for edge cases only)
--
-- The ThreatEngine (RelationshipsThreatPlates_ThreatEngine.lua) parses combat log
-- events using GlobalStrings pattern matching to calculate accurate
-- absolute threat values per mob per player, then derives precise
-- percentages as (playerThreat / topThreat) * 100.
----------------------------------------------------------------------
-- CHANGES (multi-mob threat display fix):
-- * GetThreatPercent now checks ALL party/raid members' threat
-- on each mob, not just the player's own threat
-- * Added groupThreatCache: per-mob cache of best threat from
-- any group member, so ALL attacking mobs show accurate %
-- * Added ScanGroupThreat: iterates party/raid members against
-- each visible hostile mob using native threat APIs
-- * Added ThreatEngine group parsing: CHAT_MSG_SPELL_PARTY_*
-- and CHAT_MSG_SPELL_FRIENDLYPLAYER_* events now tracked
-- * Mobs attacking any group member now show threat bars,
-- not just mobs attacking the player
-- * Previous fixes preserved: UnitAffectingCombat checks,
-- idle mob filtering, Vanilla 1.12 compatibility
----------------------------------------------------------------------
local addon = RelationshipsThreatPlates
addon.Threat = {}
-- Cache for threat values per unit identifier (name-based in 1.12)
local threatCache = {}
-- Group-wide threat cache: groupThreatCache[mobUID] = {
-- bestPct, bestSit, bestSource, mobTargetName, lastUpdate,
-- playerPct, playerSit,
-- sources = { [sourceName] = { pct, sit } }
-- }
-- This stores the BEST threat from ANY group member for each mob,
-- so all attacking mobs show accurate percentages.
local groupThreatCache = {}
-- Player name cache
local playerName = nil
-- Party/raid member names cache: name -> unitToken
local partyNames = {}
-- Party/raid unit tokens cache (ordered list)
local partyUnits = {}
-- Update throttle (seconds)
local THROTTLE = 0.2
-- ==================================================================
-- Safe API call helpers
-- ==================================================================
local function SafeCall2(func, a, b)
if not func then return nil end
local ok, r1 = pcall(func, a, b)
if ok then return r1 end
return nil
end
local function SafeCall5(func, a, b)
if not func then return nil, nil, nil, nil, nil end
local ok, r1, r2, r3, r4, r5 = pcall(func, a, b)
if ok then return r1, r2, r3, r4, r5 end
return nil, nil, nil, nil, nil
end
-- ==================================================================
-- Detect which threat APIs are available at runtime
-- ==================================================================
local hasUnitDetailedThreatSituation = false
local hasGetThreatPercent = false
local hasUnitThreatSituation = false
local function DetectThreatAPIs()
hasUnitDetailedThreatSituation = (UnitDetailedThreatSituation ~= nil)
hasGetThreatPercent = (GetThreatPercent ~= nil)
hasUnitThreatSituation = (UnitThreatSituation ~= nil)
if hasUnitDetailedThreatSituation then
addon:ui_print("UnitDetailedThreatSituation detected")
end
if hasGetThreatPercent then
addon:ui_print("GetThreatPercent detected")
end
if hasUnitThreatSituation then
addon:ui_print("UnitThreatSituation detected")
end
if not hasUnitDetailedThreatSituation and not hasGetThreatPercent and not hasUnitThreatSituation then
addon:ui_print("No native threat API - using combat log parsing + heuristic")
end
end
-- ==================================================================
-- Cache party/raid member names for threat heuristic
-- ==================================================================
local function UpdatePartyNames()
partyNames = {}
partyUnits = {}
-- Player
if playerName then
partyNames[playerName] = "player"
partyUnits[1] = "player"
end
-- Party
for i = 1, 4 do
local unit = "party" .. i
if UnitExists(unit) then
local ok, name = pcall(UnitName, unit)
if ok and name then
partyNames[name] = unit
partyUnits[table.getn(partyUnits) + 1] = unit
end
end
end
-- Raid
for i = 1, 40 do
local unit = "raid" .. i
if UnitExists(unit) then
local ok, name = pcall(UnitName, unit)
if ok and name then
partyNames[name] = unit
partyUnits[table.getn(partyUnits) + 1] = unit
end
end
end
end
-- ==================================================================
-- Scan group threat: check all party/raid members' threat on
-- a specific mob unit using native threat APIs.
--
-- Returns: bestPct, bestSit, bestSourceName, playerPct, playerSit
-- bestPct = highest threat % from any group member
-- bestSit = threat situation of the member with highest threat
-- bestSourceName = name of the group member with highest threat
-- playerPct = the player's own threat % on this mob
-- playerSit = the player's own threat situation
--
-- If no native APIs are available, returns nil (caller should
-- fall through to ThreatEngine / heuristic).
-- ==================================================================
local function ScanGroupThreatOnUnit(mobUnit)
if not mobUnit or not UnitExists(mobUnit) then
return nil
end
-- Only scan if at least one native threat API is available
if not hasUnitDetailedThreatSituation and not hasGetThreatPercent
and not hasUnitThreatSituation then
return nil
end
local bestPct = 0
local bestSit = 0
local bestSource = nil
local playerPct = 0
local playerSit = 0
for i = 1, table.getn(partyUnits) do
local groupUnit = partyUnits[i]
if UnitExists(groupUnit) then
local memberPct = nil
local memberSit = nil
-- Method 1: UnitDetailedThreatSituation
if hasUnitDetailedThreatSituation then
local isTanking, status, scaledPct, rawPct, threatVal =
SafeCall5(UnitDetailedThreatSituation, groupUnit, mobUnit)
if rawPct then
memberPct = math.min(100, math.max(0, rawPct))
memberSit = status or 0
end
end
-- Method 2: GetThreatPercent
if not memberPct and hasGetThreatPercent then
local pct = SafeCall2(GetThreatPercent, groupUnit, mobUnit)
if pct then
memberPct = math.min(100, math.max(0, pct))
if hasUnitThreatSituation then
memberSit = SafeCall2(UnitThreatSituation, groupUnit, mobUnit) or 0
else
if memberPct >= 100 then memberSit = 3
elseif memberPct >= 60 then memberSit = 2
elseif memberPct >= 30 then memberSit = 1
else memberSit = 0 end
end
end
end
-- Method 3: UnitThreatSituation
if not memberPct and hasUnitThreatSituation then
memberSit = SafeCall2(UnitThreatSituation, groupUnit, mobUnit) or 0
if memberSit == 0 then
memberPct = 0
elseif memberSit == 1 then
memberPct = 25
elseif memberSit == 2 then
memberPct = 60
elseif memberSit == 3 then
memberPct = 100
end
end
if memberPct then
-- Track player's own threat specifically
if groupUnit == "player" then
playerPct = memberPct
playerSit = memberSit or 0
end
-- Track the best threat from any group member
if memberPct > bestPct then
bestPct = memberPct
bestSit = memberSit or 0
local ok, name = pcall(UnitName, groupUnit)
bestSource = (ok and name) or groupUnit
end
end
end
end
return bestPct, bestSit, bestSource, playerPct, playerSit
end
-- ==================================================================
-- Check if a name belongs to a party/raid member
-- Returns: true if the name is a group member, false otherwise
-- ==================================================================
function addon.Threat:IsPartyMember(name)
if not name then return false end
if name == playerName then return true end
if partyNames[name] then return true end
return false
end
-- ==================================================================
-- Initialize threat module
-- ==================================================================
function addon.Threat:Initialize()
playerName = UnitName("player")
-- Detect available APIs
DetectThreatAPIs()
-- Cache party names
UpdatePartyNames()
-- Polling frame for periodic threat updates
self.pollFrame = CreateFrame("Frame", "RelationshipsThreatPlatesPollFrame")
self.pollFrame.elapsed = 0
self.pollFrame:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed >= THROTTLE then
this.elapsed = 0
addon.Threat:PollThreat()
end
end)
-- Event frame for threat-related events
local eventFrame = CreateFrame("Frame", "RelationshipsThreatPlatesEventFrame")
eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
eventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
-- Safely register threat event only if API exists
if hasUnitThreatSituation or hasUnitDetailedThreatSituation then
eventFrame:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE")
end
eventFrame:SetScript("OnEvent", function()
addon.Threat:OnEvent(event)
end)
self.eventFrame = eventFrame
end
-- ==================================================================
-- Event handler
-- ==================================================================
function addon.Threat:OnEvent(ev)
if ev == "PLAYER_TARGET_CHANGED" then
self:PollThreat()
elseif ev == "UNIT_THREAT_SITUATION_UPDATE" then
local unit = arg1
if unit and UnitExists(unit) then
self:UpdateUnitThreat(unit)
end
elseif ev == "PARTY_MEMBERS_CHANGED" or ev == "RAID_ROSTER_UPDATE" then
UpdatePartyNames()
self:PollThreat()
end
end
-- ==================================================================
-- Poll threat for all visible nameplates
-- ==================================================================
function addon.Threat:PollThreat()
if not RelationshipsThreatPlatesDB or not RelationshipsThreatPlatesDB.enabled then
return
end
for nameplate, data in pairs(addon.Nameplates.plates) do
if nameplate:IsVisible() and data.unit then
self:UpdateUnitThreat(data.unit)
end
end
end
-- ==================================================================
-- Get threat percentage for a unit
-- Returns: threatPct (0-100), threatSit (0-3)
--
-- PRIORITY:
-- 1. Group-wide scan via UnitDetailedThreatSituation (Turtle WoW)
-- 2. Group-wide scan via GetThreatPercent API (custom extension)
-- 3. Group-wide scan via UnitThreatSituation (partial API)
-- 4. ThreatEngine combat log data (precise per-1% values)
-- 5. Target-based heuristic (crude fallback only)
--
-- KEY CHANGE: Methods 1-3 now check ALL party/raid members' threat
-- on the mob, not just the player. This ensures that:
-- - Mobs attacking the player show the player's threat %
-- - Mobs attacking party members show those members' threat %
-- - ALL mobs in combat with the group show accurate threat bars
----------------------------------------------------------------------
function addon.Threat:GetThreatPercent(unit)
if not unit or not UnitExists(unit) then
return 0, 0
end
-- ==============================================================
-- Pre-filter: Only check threat for hostile, in-combat mobs
-- These checks apply regardless of which method we use below.
-- ==============================================================
-- Only check threat for hostile units
local reaction = UnitReaction(unit, "player")
if reaction and reaction > 4 then
-- Friendly unit - no threat display
return 0, 0
end
-- Check if player OR any group member is in combat.
-- CRITICAL FIX: Previously only checked UnitAffectingCombat("player"),
-- which meant mobs attacking party members showed no threat when the
-- player wasn't in combat. Now we check the entire group.
local playerInCombat = UnitAffectingCombat("player")
local groupInCombat = addon.ThreatEngine:IsGroupInCombat()
-- The MOB itself must be in combat.
local ok_mc, mobCombat = pcall(UnitAffectingCombat, unit)
if ok_mc and not mobCombat then
return 0, 0
end
-- Verify this is a hostile unit the player can attack
local ok_ca, canAttack = pcall(UnitCanAttack, "player", unit)
if ok_ca and not canAttack then
-- Also check if any group member can attack it
local groupCanAttack = false
for i = 1, table.getn(partyUnits) do
local ok_gca, gca = pcall(UnitCanAttack, partyUnits[i], unit)
if ok_gca and gca then
groupCanAttack = true
break
end
end
if not groupCanAttack then
return 0, 0
end
end
-- Get mob name and who the mob is targeting (used by multiple methods)
local ok_name, mobName = pcall(UnitName, unit)
if not ok_name or not mobName then
return 0, 0
end
local mobTargetName = nil
local ok_mt, mt = pcall(UnitName, unit .. "target")
if ok_mt and mt then
mobTargetName = mt
end
-- ==============================================================
-- Methods 1-3: Group-wide native threat API scan
-- Check ALL party/raid members' threat on this mob.
-- This is the KEY fix: instead of only checking the player's
-- threat, we check every group member and return the best.
-- ==============================================================
if hasUnitDetailedThreatSituation or hasGetThreatPercent or hasUnitThreatSituation then
local bestPct, bestSit, bestSource, playerPct, playerSit =
ScanGroupThreatOnUnit(unit)
if bestPct and bestPct > 0 then
-- We have threat data from at least one group member.
-- Determine whose threat to display:
-- If the mob is targeting the player -> show player's threat
-- If the mob is targeting a group member -> show that member's threat
-- If the mob is targeting an outsider -> show player's threat
-- Otherwise -> show the best (highest) threat from any member
if mobTargetName == playerName then
-- Mob is attacking the player - show player's own threat
return playerPct, playerSit
elseif mobTargetName and partyNames[mobTargetName] then
-- Mob is attacking a group member - show that member's threat
-- Look up that member's threat from the group scan
local memberUnit = partyNames[mobTargetName]
local memberPct = nil
local memberSit = nil
if hasUnitDetailedThreatSituation then
local _, status, _, rawPct = SafeCall5(UnitDetailedThreatSituation, memberUnit, unit)
if rawPct then
memberPct = math.min(100, math.max(0, rawPct))
memberSit = status or 0
end
end
if not memberPct and hasGetThreatPercent then
local pct = SafeCall2(GetThreatPercent, memberUnit, unit)
if pct then
memberPct = math.min(100, math.max(0, pct))
if hasUnitThreatSituation then
memberSit = SafeCall2(UnitThreatSituation, memberUnit, unit) or 0
else
if memberPct >= 100 then memberSit = 3
elseif memberPct >= 60 then memberSit = 2
elseif memberPct >= 30 then memberSit = 1
else memberSit = 0 end
end
end
end
if not memberPct and hasUnitThreatSituation then
memberSit = SafeCall2(UnitThreatSituation, memberUnit, unit) or 0
if memberSit == 0 then memberPct = 0
elseif memberSit == 1 then memberPct = 25
elseif memberSit == 2 then memberPct = 60
elseif memberSit == 3 then memberPct = 100 end
end
if memberPct then
return memberPct, memberSit or 0
end
-- Fallback: can't get specific member data, use best
return bestPct, bestSit
else
-- Mob targets an outsider, or we can't see its target.
-- Show player's threat if they have any, otherwise the
-- best group threat.
if playerPct > 0 then
return playerPct, playerSit
end
return bestPct, bestSit
end
end
-- All group members have 0 threat on this mob via native APIs,
-- but the mob IS in combat. This can happen if:
-- - Only ThreatEngine has data (combat log parsing)
-- - The mob is fighting an outsider
-- - The mob was just pulled and the API hasn't updated yet
-- Fall through to ThreatEngine / heuristic below.
end
-- ==============================================================
-- Method 4: ThreatEngine combat log parsing
-- This provides precise per-1% threat values based on actual
-- damage/healing/ability threat tracked from the combat log.
-- This is the PRIMARY method on servers without native APIs.
--
-- KEY FIX: Now uses group combat state instead of only player combat.
-- If any group member is in combat, ThreatEngine data is relevant.
-- ==============================================================
-- Someone in the group must be in combat for ThreatEngine data
-- to be relevant. This prevents threat bars on idle mobs when
-- no one in the group is fighting.
if not groupInCombat then
return 0, 0
end
-- If the mob targets an outsider (someone not in the
-- player's party/raid), verify the player has actually engaged
-- this mob personally before showing any threat bar.
if mobTargetName and mobTargetName ~= playerName and not partyNames[mobTargetName] then
-- Mob targets someone outside our group.
-- Check if the player has personally hit this mob.
-- If player has 0 threat on this mob, don't show a bar.
local playerThreat = addon.ThreatEngine:GetPlayerThreat(mobName)
if playerThreat <= 0 then
return 0, 0
end
-- Player has some threat but mob targets an outsider.
-- The player's threat is secondary - show it but reduced.
-- We can't know the outsider's threat, so estimate conservatively.
-- Use ThreatEngine data if available, otherwise heuristic.
local enginePct, engineSit, _, _, hasGroupData = addon.ThreatEngine:GetThreatOnTarget(mobName)
if enginePct > 0 then
if hasGroupData then
return enginePct, engineSit
end
-- Solo data only - the outsider probably has more threat.
-- Cap our displayed percentage conservatively.
if enginePct >= 100 then
return 80, 2
end
return enginePct, engineSit
end
-- No ThreatEngine data but player is hitting it - rough estimate
if UnitIsUnit("target", unit) then
return 40, 1
end
return 20, 0
end
-- Query ThreatEngine with the mob's name
local enginePct, engineSit, enginePlayerThreat, engineTopThreat, hasGroupData =
addon.ThreatEngine:GetThreatOnTarget(mobName)
if enginePct > 0 then
if hasGroupData then
-- We have threat data from multiple sources (group broadcasts).
-- The percentage is reliable - use it directly.
return enginePct, engineSit
end
-- ThreatEngine only has the player's own threat data (no group
-- broadcasts). Cross-reference with mob's target to calibrate.
if mobTargetName then
if mobTargetName == playerName then
-- Mob targets the player AND player has threat data.
-- Player really does have aggro. Since we only know our
-- own threat and we're the top, show as 100%.
return 100, 3
elseif partyNames[mobTargetName] then
-- Mob targets a PARTY/RAID member. The player doesn't
-- have aggro but does have some threat. Show the
-- ThreatEngine percentage as the player's relative threat.
-- This is much better than the old fixed 60%/30% estimate.
return enginePct, engineSit
else
-- Mob targets an outsider - show conservative estimate
if enginePct >= 100 then
return 90, 2
end
return enginePct, engineSit
end
else
-- Can't see mob's target. Show raw ThreatEngine data
-- but cap at 90% since we can't confirm we have aggro.
if enginePct >= 100 then
return 90, 2
end
return enginePct, engineSit
end
end
-- ==============================================================
-- Method 5: Target-based heuristic (last resort fallback)
-- Only used when ThreatEngine has NO data for a mob.
-- This can happen for: newly engaged mobs, mobs in combat with
-- other players outside our group, or very brief encounters.
--
-- This is the MOST RELIABLE heuristic in Vanilla WoW:
-- Who is the mob targeting? That person has aggro.
--
-- KEY CHANGE: Now properly handles party/raid members.
-- If the mob is targeting a group member, we show a high
-- threat bar (that member has aggro), not a low estimate.
-- ==============================================================
-- Check if the mob is targeting someone
if mobTargetName then
if mobTargetName == playerName then
-- Mob is targeting the PLAYER - player has AGGRO (100%)
threatPct = 100
situation = 3
elseif partyNames[mobTargetName] then
-- Mob is targeting a PARTY/RAID member.
-- That member has aggro - show high threat.
-- This is the KEY fix: previously showed only 60%/30%
-- for mobs targeting group members. Now shows 100%
-- because that group member IS the one being attacked.
threatPct = 100
situation = 3
else
-- Mob is targeting someone outside the party
-- (another player, a pet, etc.)
if UnitIsUnit("target", unit) then
threatPct = 40
situation = 1
else
threatPct = 15
situation = 0
end
end
else
-- Mob's target is nil. We already know it's in combat.
-- This happens when someone outside our group has aggro
-- or the mob is between targets.
if UnitIsUnit("target", unit) then
threatPct = 40
situation = 1
else
threatPct = 10
situation = 0
end
end
return threatPct, situation
end
-- ==================================================================
-- Update threat for a specific unit and notify nameplates
-- ==================================================================
function addon.Threat:UpdateUnitThreat(unit)
if not unit or not UnitExists(unit) then return end
local pct, situation = self:GetThreatPercent(unit)
local uid = addon:GetUnitID(unit)
if uid then
-- Get mob target info for group context
local mobTargetName = nil
local ok_mt, mt = pcall(UnitName, unit .. "target")
if ok_mt and mt then
mobTargetName = mt
end
threatCache[uid] = {
pct = pct,
situation = situation,
unit = unit,
lastUpdate = GetTime(),
mobTargetName = mobTargetName,
}
-- Also store in group threat cache for cross-reference
groupThreatCache[uid] = {
bestPct = pct,
bestSit = situation,
mobTargetName = mobTargetName,
lastUpdate = GetTime(),
}
addon.Nameplates:UpdateThreatForUnit(unit, pct, situation)
end
end
-- ==================================================================
-- Get cached threat for a unit identifier
-- ==================================================================
function addon.Threat:GetCachedThreat(uid)
if not uid then return 0, 0 end
local cache = threatCache[uid]
if cache then
if GetTime() - cache.lastUpdate < 5 then
return cache.pct, cache.situation
end
end
return 0, 0
end
-- ==================================================================
-- Get threat percentage by mob NAME (no unit token required)
-- This is the fallback for when FindUnitByName couldn't resolve
-- a unit token for a nameplate.
--
-- CRITICAL FIX for same-name mob confusion:
-- In Vanilla 1.12 there is no UnitGUID. Multiple mobs with the
-- same name (e.g. "Defias Bandit") all share one ThreatEngine
-- entry. If we return threat data by name alone, idle same-name
-- mobs will incorrectly show threat bars.
--
-- Solution: GetThreatByName now REQUIRES finding a unit token
-- that is verified to be in combat. If no combat-verified unit
-- token can be found for the given name, it returns 0, 0.
-- This ensures idle mobs never get threat data.
--
-- Returns: threatPct (0-100), threatSit (0-3)
-- ==================================================================
function addon.Threat:GetThreatByName(mobName)
if not mobName or strlen(mobName) == 0 then
return 0, 0
end
-- KEY FIX: Only require that SOMEONE in the group is in combat,
-- not necessarily the player. This allows threat bars to show
-- on mobs attacking party/raid members even when the player
-- isn't in combat themselves.
if not addon.ThreatEngine:IsGroupInCombat() then
return 0, 0
end
-- ==============================================================
-- Step 1: Try to find a combat-verified unit token.
-- If we can find one and verify it's in combat, use the full
-- GetThreatPercent which has all the proper checks.
-- ==============================================================
local unitTokens = {
"target", "mouseover", "targettarget", "pet",
}
for i = 1, 4 do
unitTokens[table.getn(unitTokens) + 1] = "party" .. i .. "target"
unitTokens[table.getn(unitTokens) + 1] = "party" .. i .. "targettarget"
end
for i = 1, 40 do
unitTokens[table.getn(unitTokens) + 1] = "raid" .. i .. "target"
unitTokens[table.getn(unitTokens) + 1] = "raid" .. i .. "targettarget"
end
-- Search for a unit token that matches the name AND is in combat
for i = 1, table.getn(unitTokens) do
local token = unitTokens[i]
local ok, exists = pcall(UnitExists, token)
if ok and exists then
local ok2, name = pcall(UnitName, token)
if ok2 and name and name == mobName then
local ok3, inCombat = pcall(UnitAffectingCombat, token)
if ok3 and inCombat then
local pct, sit = self:GetThreatPercent(token)
return pct, sit
end
end
end
end
-- ==============================================================
-- Step 2: No combat-verified unit token found.
--
-- For UNIQUE-name mobs (only 1 plate with this name visible),
-- the ThreatEngine data IS unambiguous — there's only one mob
-- with this name, so the threat data is definitely about it.
-- We can use ThreatEngine data directly.
--
-- For SAME-name mobs (multiple plates share this name), we
-- CANNOT use ThreatEngine data without a unit token because
-- it would apply the same threat to all same-name plates,
-- including idle ones.
--
-- The caller (UpdateAllPlates) decides whether a name is
-- unique. GetThreatByName is ONLY called for unique names
-- from UpdateAllPlates, so it's safe to use ThreatEngine here.
-- ==============================================================
if addon.ThreatEngine:HasDataForTarget(mobName) then
local enginePct, engineSit, _, _, hasGroupData =
addon.ThreatEngine:GetThreatOnTarget(mobName)
if enginePct > 0 then
-- We have ThreatEngine data for this unique-name mob.
-- Cap at 90% since we can't verify who has aggro
-- without seeing the mob's target.
if enginePct >= 100 then
return 90, 2
end
return enginePct, engineSit
end
end
-- No ThreatEngine data either. This mob likely hasn't been
-- engaged by the player. Return 0.
return 0, 0
end
-- ==================================================================
-- Check if a mob is attacking any group member
-- Returns: the name of the group member being attacked, or nil
-- ==================================================================
function addon.Threat:GetMobTarget(mobUnit)
if not mobUnit or not UnitExists(mobUnit) then
return nil
end
local ok, targetName = pcall(UnitName, mobUnit .. "target")
if ok and targetName then
if targetName == playerName or partyNames[targetName] then
return targetName
end
end
return nil
end
-- ==================================================================
-- Get cached group threat for a unit identifier
-- Returns: bestPct, bestSit, mobTargetName
-- ==================================================================
function addon.Threat:GetGroupCachedThreat(uid)
if not uid then return 0, 0, nil end
local cache = groupThreatCache[uid]
if cache then
if GetTime() - cache.lastUpdate < 5 then
return cache.bestPct, cache.bestSit, cache.mobTargetName
end
end
return 0, 0, nil
end
-- ==================================================================
-- Clear all threat data
-- ==================================================================
function addon.Threat:ClearAll()
threatCache = {}
groupThreatCache = {}
end
File diff suppressed because it is too large Load Diff