Files
RelationshipsThreatPlates/RelationshipsThreatPlates_Threat.lua
T
2026-07-12 14:57:27 +01:00

933 lines
36 KiB
Lua

----------------------------------------------------------------------
-- 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.1
-- Cache of per-mob tank-mode details, keyed by mob UID (name in vanilla).
-- Populated by UpdateUnitThreat via ThreatEngine, and read by the Nameplates
-- module when rendering the tank-mode text (raw threat lead).
local tankDetailsCache = {}
-- ==================================================================
-- 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
-- The threat bar on a nameplate is always displayed from the
-- PLAYER's point of view: "how close am I to pulling aggro
-- off whoever currently has it?". Showing another group
-- member's percentage would incorrectly make every mob a
-- party member is tanking read as 100% — that is the bug
-- users describe as "snap causes all party-combat enemies
-- to show 100%". So: always return the player's own value
-- here. If the player has no threat on this mob, return 0
-- so the bar hides.
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 the player's
-- own relative threat, not the party member's.
if playerPct > 0 then
return playerPct, playerSit
end
-- Player has no threat yet, but a party member is in
-- the aggro seat. Show a low visible bar so the plate
-- is still displayed (bar hides completely at pct=0).
return 8, 0
else
-- Mob targets an outsider, or we can't see its target.
-- Show player's own threat only.
if playerPct > 0 then
return playerPct, playerSit
end
-- Fall through: let ThreatEngine / heuristic decide
-- below instead of hiding the bar outright.
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. The party member
-- has aggro, NOT the player. Showing 100% here would
-- incorrectly light every mob in party combat up as full
-- aggro on the player. Show a low estimate instead — the
-- player is not in the aggro seat, but the bar must still
-- be visible so users know the mob is engaged.
if UnitIsUnit("target", unit) then
threatPct = 20
situation = 0
else
threatPct = 8
situation = 0
end
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
-- ------------------------------------------------------------
-- TARGET-SNAP override: if the mob is currently targeting the
-- player, force display to 100% (aggro). Some mobs pick focused
-- / random targets regardless of threat, so the plate should
-- immediately snap to 100% while targeted, and fall back to the
-- real threat % when the mob switches targets.
-- ------------------------------------------------------------
local targetSnapped = false
if RelationshipsThreatPlatesDB and RelationshipsThreatPlatesDB.targetSnap100
and mobTargetName and playerName and mobTargetName == playerName then
local ok_pc, pc = pcall(UnitAffectingCombat, "player")
if ok_pc and pc then
-- Player is being targeted, so they have aggro. Force
-- pct to at least 100 without clobbering a real value
-- above 100 (which happens when the player is top and
-- well ahead of the second-highest threat holder).
if not pct or pct < 100 then
pct = 100
end
situation = 3
targetSnapped = true
end
end
-- ------------------------------------------------------------
-- Compute raw tank-mode details (player threat vs 2nd highest).
-- Uses ThreatEngine's combat-log-parsed absolute values when
-- available. This is what the nameplate renders in Tank Mode.
-- ------------------------------------------------------------
local ok_mn, mobName = pcall(UnitName, unit)
if ok_mn and mobName then
local ePct, eSit, ePlayerThreat, eTopThreat, eHasGroup, eTopPlayer, eSecondThreat =
addon.ThreatEngine:GetThreatOnTarget(mobName)
tankDetailsCache[uid] = {
playerThreat = ePlayerThreat or 0,
topThreat = eTopThreat or 0,
secondThreat = eSecondThreat or 0,
topPlayer = eTopPlayer,
hasAggro = (eTopPlayer == playerName) or targetSnapped,
hasGroupData = eHasGroup or false,
targetSnapped = targetSnapped,
mobTargetName = mobTargetName,
lastUpdate = GetTime(),
}
end
threatCache[uid] = {
pct = pct,
situation = situation,
unit = unit,
lastUpdate = GetTime(),
mobTargetName = mobTargetName,
targetSnapped = targetSnapped,
}
-- 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
-- ==================================================================
-- Tank-mode details for a mob (by UID / name in vanilla).
-- Returns a table (or nil) with:
-- playerThreat, topThreat, secondThreat, topPlayer,
-- hasAggro, hasGroupData, targetSnapped, mobTargetName
-- ==================================================================
function addon.Threat:GetTankDetails(uid)
if not uid then return nil end
local d = tankDetailsCache[uid]
if not d then return nil end
if GetTime() - d.lastUpdate > 5 then return nil end
return d
end
-- ==================================================================
-- Format a raw threat value as a compact signed string.
-- Examples: 12345 -> "+12.3K", -845 -> "-845", 0 -> "0"
-- ==================================================================
function addon.Threat:FormatThreatLead(v)
if not v then return "--" end
local a = math.abs(v)
local sign = ""
if v > 0 then sign = "+" elseif v < 0 then sign = "-" end
if a >= 100000 then
return sign .. string.format("%.0fK", a / 1000)
elseif a >= 10000 then
return sign .. string.format("%.1fK", a / 1000)
elseif a >= 1000 then
return sign .. string.format("%.2fK", a / 1000)
else
return sign .. string.format("%d", a)
end
end
-- ==================================================================
-- Return the display string for tank mode given a plate's mob name.
-- Uses the ThreatEngine directly (works even without a unit token,
-- e.g. for unique-name plates).
-- Returns: string like "+1.20K" or "-450", or "--" if unknown.
-- ==================================================================
function addon.Threat:GetTankDisplayForMob(mobName)
if not mobName then return "--" end
local d = tankDetailsCache[mobName]
if not d or (GetTime() - d.lastUpdate > 5) then
-- Query ThreatEngine on demand (no cache hit).
local _, _, playerThreat, topThreat, _, topPlayer, secondThreat =
addon.ThreatEngine:GetThreatOnTarget(mobName)
if not playerThreat or playerThreat <= 0 then
return "--"
end
local lead
if topPlayer == playerName then
lead = playerThreat - (secondThreat or 0)
else
lead = playerThreat - (topThreat or 0)
end
return self:FormatThreatLead(lead)
end
if d.playerThreat <= 0 then return "--" end
local lead
if d.hasAggro then
lead = d.playerThreat - (d.secondThreat or 0)
else
lead = d.playerThreat - (d.topThreat or 0)
end
return self:FormatThreatLead(lead)
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