Add files via upload
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
----------------------------------------------------------------------
|
||||
-- ThreatPlates
|
||||
-- Threat percentage bars above nameplates
|
||||
-- Styled after Kui Nameplates
|
||||
-- Standalone - no dependencies required
|
||||
----------------------------------------------------------------------
|
||||
## Interface: 11200
|
||||
## Title: ThreatPlates
|
||||
## Title-zhCN: 威胁姓名板
|
||||
## Notes: Displays threat percentage bars above nameplates with Kui-style visuals
|
||||
## Notes-zhCN: 在姓名板上方显示威胁百分比条
|
||||
## Author: Relationship
|
||||
## Version: 2.3.2
|
||||
## SavedVariables: ThreatPlatesDB
|
||||
|
||||
ThreatPlates.xml
|
||||
@@ -0,0 +1,8 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script file="ThreatPlates_Core.lua"/>
|
||||
<Script file="ThreatPlates_Media.lua"/>
|
||||
<Script file="ThreatPlates_ThreatEngine.lua"/>
|
||||
<Script file="ThreatPlates_Threat.lua"/>
|
||||
<Script file="ThreatPlates_Nameplates.lua"/>
|
||||
<Script file="ThreatPlates_GUI.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,177 @@
|
||||
----------------------------------------------------------------------
|
||||
-- ThreatPlates_Core.lua
|
||||
-- Core addon initialization, saved variables, slash commands
|
||||
-- Compatible with WoW 1.12 (Vanilla / Turtle WoW / Octo WoW)
|
||||
----------------------------------------------------------------------
|
||||
|
||||
-- Create the global addon object
|
||||
ThreatPlates = CreateFrame("Frame")
|
||||
|
||||
local addon = ThreatPlates
|
||||
addon.MAJOR = 1
|
||||
addon.MINOR = 1
|
||||
|
||||
-- Version string
|
||||
TP_VERSION = "2.3.1"
|
||||
TP_ADDON_NAME = "ThreatPlates"
|
||||
|
||||
-- ==================================================================
|
||||
-- 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("|cffbb99ffThreatPlates|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 ThreatPlatesDB then
|
||||
ThreatPlatesDB = DeepCopy(DEFAULT_CONFIG)
|
||||
else
|
||||
-- Merge in any missing keys from defaults
|
||||
for k, v in pairs(DEFAULT_CONFIG) do
|
||||
if ThreatPlatesDB[k] == nil then
|
||||
ThreatPlatesDB[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 ThreatPlatesDB and ThreatPlatesDB.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
|
||||
ThreatPlatesDB = DeepCopy(DEFAULT_CONFIG)
|
||||
addon:ui_print("Settings reset to defaults.")
|
||||
addon.GUI:Refresh()
|
||||
elseif msg == "toggle" then
|
||||
ThreatPlatesDB.enabled = not ThreatPlatesDB.enabled
|
||||
local state = ThreatPlatesDB.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
|
||||
@@ -0,0 +1,499 @@
|
||||
----------------------------------------------------------------------
|
||||
-- ThreatPlates_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 = ThreatPlates
|
||||
|
||||
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", "ThreatPlatesGUI", 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()
|
||||
ThreatPlatesDB.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()
|
||||
ThreatPlatesDB.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("ThreatPlates")
|
||||
|
||||
-- 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", "ThreatPlatesScrollFrame", 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", "ThreatPlatesScrollBar", 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", "ThreatPlatesScrollChild", 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 ThreatPlatesDB and ThreatPlatesDB.guiPos then
|
||||
local pos = ThreatPlatesDB.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 ThreatPlatesDB[configKey] then
|
||||
btn.checked = true
|
||||
check:Show()
|
||||
end
|
||||
|
||||
btn:SetScript("OnMouseDown", function()
|
||||
this.checked = not this.checked
|
||||
ThreatPlatesDB[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", "ThreatPlatesSlider_" .. 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(ThreatPlatesDB[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 = ThreatPlatesDB[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
|
||||
ThreatPlatesDB[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 = ThreatPlatesDB[key]
|
||||
widget.checked = val
|
||||
if val then
|
||||
widget.checkMark:Show()
|
||||
else
|
||||
widget.checkMark:Hide()
|
||||
end
|
||||
-- Slider
|
||||
elseif widget.SetValue then
|
||||
widget:SetValue(ThreatPlatesDB[key])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
----------------------------------------------------------------------
|
||||
-- ThreatPlates_Media.lua
|
||||
-- Embedded media textures and fonts
|
||||
-- Self-contained - generates textures procedurally, no external files
|
||||
----------------------------------------------------------------------
|
||||
|
||||
local addon = ThreatPlates -- Created in ThreatPlates_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\\ThreatPlates\\Media\\Bar"
|
||||
addon.Media.BG_TEXTURE = "Interface\\AddOns\\ThreatPlates\\Media\\BarBG"
|
||||
addon.Media.SOLID_TEXTURE = "Interface\\Buttons\\WHITE8x8"
|
||||
addon.Media.GLOW_TEXTURE = "Interface\\AddOns\\ThreatPlates\\Media\\Glow"
|
||||
addon.Media.SPARK_TEXTURE = "Interface\\AddOns\\ThreatPlates\\Media\\Spark"
|
||||
addon.Media.SHADOW_TEXTURE = "Interface\\AddOns\\ThreatPlates\\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
@@ -0,0 +1,828 @@
|
||||
----------------------------------------------------------------------
|
||||
-- ThreatPlates_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 (ThreatPlates_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 = ThreatPlates
|
||||
|
||||
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", "ThreatPlatesPollFrame")
|
||||
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", "ThreatPlatesEventFrame")
|
||||
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 ThreatPlatesDB or not ThreatPlatesDB.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
Reference in New Issue
Block a user