928 lines
36 KiB
Lua
928 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
|
|
return 0, 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
|
|
return 0, 0
|
|
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.
|
|
if UnitIsUnit("target", unit) then
|
|
threatPct = 20
|
|
situation = 0
|
|
else
|
|
threatPct = 0
|
|
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
|