Add files via upload
This commit is contained in:
@@ -0,0 +1,828 @@
|
||||
----------------------------------------------------------------------
|
||||
-- RelationshipsThreatPlates_Threat.lua
|
||||
-- Threat calculation bridge for WoW 1.12 (Vanilla API)
|
||||
-- Works with Turtle WoW / Octo WoW threat hooks
|
||||
-- Standalone - does not require Omen, KLHThreatMeter, or any lib
|
||||
--
|
||||
-- THREAT DATA SOURCES (in priority order):
|
||||
-- 1. UnitDetailedThreatSituation (Turtle WoW server extension)
|
||||
-- 2. GetThreatPercent API (custom server extension)
|
||||
-- 3. UnitThreatSituation (partial API on some servers)
|
||||
-- 4. ThreatEngine combat log parsing (precise per-1% values)
|
||||
-- 5. Target-based heuristic (crude fallback for edge cases only)
|
||||
--
|
||||
-- The ThreatEngine (RelationshipsThreatPlates_ThreatEngine.lua) parses combat log
|
||||
-- events using GlobalStrings pattern matching to calculate accurate
|
||||
-- absolute threat values per mob per player, then derives precise
|
||||
-- percentages as (playerThreat / topThreat) * 100.
|
||||
----------------------------------------------------------------------
|
||||
-- CHANGES (multi-mob threat display fix):
|
||||
-- * GetThreatPercent now checks ALL party/raid members' threat
|
||||
-- on each mob, not just the player's own threat
|
||||
-- * Added groupThreatCache: per-mob cache of best threat from
|
||||
-- any group member, so ALL attacking mobs show accurate %
|
||||
-- * Added ScanGroupThreat: iterates party/raid members against
|
||||
-- each visible hostile mob using native threat APIs
|
||||
-- * Added ThreatEngine group parsing: CHAT_MSG_SPELL_PARTY_*
|
||||
-- and CHAT_MSG_SPELL_FRIENDLYPLAYER_* events now tracked
|
||||
-- * Mobs attacking any group member now show threat bars,
|
||||
-- not just mobs attacking the player
|
||||
-- * Previous fixes preserved: UnitAffectingCombat checks,
|
||||
-- idle mob filtering, Vanilla 1.12 compatibility
|
||||
----------------------------------------------------------------------
|
||||
|
||||
local addon = RelationshipsThreatPlates
|
||||
|
||||
addon.Threat = {}
|
||||
|
||||
-- Cache for threat values per unit identifier (name-based in 1.12)
|
||||
local threatCache = {}
|
||||
|
||||
-- Group-wide threat cache: groupThreatCache[mobUID] = {
|
||||
-- bestPct, bestSit, bestSource, mobTargetName, lastUpdate,
|
||||
-- playerPct, playerSit,
|
||||
-- sources = { [sourceName] = { pct, sit } }
|
||||
-- }
|
||||
-- This stores the BEST threat from ANY group member for each mob,
|
||||
-- so all attacking mobs show accurate percentages.
|
||||
local groupThreatCache = {}
|
||||
|
||||
-- Player name cache
|
||||
local playerName = nil
|
||||
|
||||
-- Party/raid member names cache: name -> unitToken
|
||||
local partyNames = {}
|
||||
|
||||
-- Party/raid unit tokens cache (ordered list)
|
||||
local partyUnits = {}
|
||||
|
||||
-- Update throttle (seconds)
|
||||
local THROTTLE = 0.2
|
||||
|
||||
-- ==================================================================
|
||||
-- Safe API call helpers
|
||||
-- ==================================================================
|
||||
local function SafeCall2(func, a, b)
|
||||
if not func then return nil end
|
||||
local ok, r1 = pcall(func, a, b)
|
||||
if ok then return r1 end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function SafeCall5(func, a, b)
|
||||
if not func then return nil, nil, nil, nil, nil end
|
||||
local ok, r1, r2, r3, r4, r5 = pcall(func, a, b)
|
||||
if ok then return r1, r2, r3, r4, r5 end
|
||||
return nil, nil, nil, nil, nil
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Detect which threat APIs are available at runtime
|
||||
-- ==================================================================
|
||||
local hasUnitDetailedThreatSituation = false
|
||||
local hasGetThreatPercent = false
|
||||
local hasUnitThreatSituation = false
|
||||
|
||||
local function DetectThreatAPIs()
|
||||
hasUnitDetailedThreatSituation = (UnitDetailedThreatSituation ~= nil)
|
||||
hasGetThreatPercent = (GetThreatPercent ~= nil)
|
||||
hasUnitThreatSituation = (UnitThreatSituation ~= nil)
|
||||
|
||||
if hasUnitDetailedThreatSituation then
|
||||
addon:ui_print("UnitDetailedThreatSituation detected")
|
||||
end
|
||||
if hasGetThreatPercent then
|
||||
addon:ui_print("GetThreatPercent detected")
|
||||
end
|
||||
if hasUnitThreatSituation then
|
||||
addon:ui_print("UnitThreatSituation detected")
|
||||
end
|
||||
if not hasUnitDetailedThreatSituation and not hasGetThreatPercent and not hasUnitThreatSituation then
|
||||
addon:ui_print("No native threat API - using combat log parsing + heuristic")
|
||||
end
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Cache party/raid member names for threat heuristic
|
||||
-- ==================================================================
|
||||
local function UpdatePartyNames()
|
||||
partyNames = {}
|
||||
partyUnits = {}
|
||||
-- Player
|
||||
if playerName then
|
||||
partyNames[playerName] = "player"
|
||||
partyUnits[1] = "player"
|
||||
end
|
||||
-- Party
|
||||
for i = 1, 4 do
|
||||
local unit = "party" .. i
|
||||
if UnitExists(unit) then
|
||||
local ok, name = pcall(UnitName, unit)
|
||||
if ok and name then
|
||||
partyNames[name] = unit
|
||||
partyUnits[table.getn(partyUnits) + 1] = unit
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Raid
|
||||
for i = 1, 40 do
|
||||
local unit = "raid" .. i
|
||||
if UnitExists(unit) then
|
||||
local ok, name = pcall(UnitName, unit)
|
||||
if ok and name then
|
||||
partyNames[name] = unit
|
||||
partyUnits[table.getn(partyUnits) + 1] = unit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Scan group threat: check all party/raid members' threat on
|
||||
-- a specific mob unit using native threat APIs.
|
||||
--
|
||||
-- Returns: bestPct, bestSit, bestSourceName, playerPct, playerSit
|
||||
-- bestPct = highest threat % from any group member
|
||||
-- bestSit = threat situation of the member with highest threat
|
||||
-- bestSourceName = name of the group member with highest threat
|
||||
-- playerPct = the player's own threat % on this mob
|
||||
-- playerSit = the player's own threat situation
|
||||
--
|
||||
-- If no native APIs are available, returns nil (caller should
|
||||
-- fall through to ThreatEngine / heuristic).
|
||||
-- ==================================================================
|
||||
local function ScanGroupThreatOnUnit(mobUnit)
|
||||
if not mobUnit or not UnitExists(mobUnit) then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Only scan if at least one native threat API is available
|
||||
if not hasUnitDetailedThreatSituation and not hasGetThreatPercent
|
||||
and not hasUnitThreatSituation then
|
||||
return nil
|
||||
end
|
||||
|
||||
local bestPct = 0
|
||||
local bestSit = 0
|
||||
local bestSource = nil
|
||||
local playerPct = 0
|
||||
local playerSit = 0
|
||||
|
||||
for i = 1, table.getn(partyUnits) do
|
||||
local groupUnit = partyUnits[i]
|
||||
if UnitExists(groupUnit) then
|
||||
local memberPct = nil
|
||||
local memberSit = nil
|
||||
|
||||
-- Method 1: UnitDetailedThreatSituation
|
||||
if hasUnitDetailedThreatSituation then
|
||||
local isTanking, status, scaledPct, rawPct, threatVal =
|
||||
SafeCall5(UnitDetailedThreatSituation, groupUnit, mobUnit)
|
||||
if rawPct then
|
||||
memberPct = math.min(100, math.max(0, rawPct))
|
||||
memberSit = status or 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Method 2: GetThreatPercent
|
||||
if not memberPct and hasGetThreatPercent then
|
||||
local pct = SafeCall2(GetThreatPercent, groupUnit, mobUnit)
|
||||
if pct then
|
||||
memberPct = math.min(100, math.max(0, pct))
|
||||
if hasUnitThreatSituation then
|
||||
memberSit = SafeCall2(UnitThreatSituation, groupUnit, mobUnit) or 0
|
||||
else
|
||||
if memberPct >= 100 then memberSit = 3
|
||||
elseif memberPct >= 60 then memberSit = 2
|
||||
elseif memberPct >= 30 then memberSit = 1
|
||||
else memberSit = 0 end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Method 3: UnitThreatSituation
|
||||
if not memberPct and hasUnitThreatSituation then
|
||||
memberSit = SafeCall2(UnitThreatSituation, groupUnit, mobUnit) or 0
|
||||
if memberSit == 0 then
|
||||
memberPct = 0
|
||||
elseif memberSit == 1 then
|
||||
memberPct = 25
|
||||
elseif memberSit == 2 then
|
||||
memberPct = 60
|
||||
elseif memberSit == 3 then
|
||||
memberPct = 100
|
||||
end
|
||||
end
|
||||
|
||||
if memberPct then
|
||||
-- Track player's own threat specifically
|
||||
if groupUnit == "player" then
|
||||
playerPct = memberPct
|
||||
playerSit = memberSit or 0
|
||||
end
|
||||
|
||||
-- Track the best threat from any group member
|
||||
if memberPct > bestPct then
|
||||
bestPct = memberPct
|
||||
bestSit = memberSit or 0
|
||||
local ok, name = pcall(UnitName, groupUnit)
|
||||
bestSource = (ok and name) or groupUnit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return bestPct, bestSit, bestSource, playerPct, playerSit
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Check if a name belongs to a party/raid member
|
||||
-- Returns: true if the name is a group member, false otherwise
|
||||
-- ==================================================================
|
||||
function addon.Threat:IsPartyMember(name)
|
||||
if not name then return false end
|
||||
if name == playerName then return true end
|
||||
if partyNames[name] then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Initialize threat module
|
||||
-- ==================================================================
|
||||
function addon.Threat:Initialize()
|
||||
playerName = UnitName("player")
|
||||
|
||||
-- Detect available APIs
|
||||
DetectThreatAPIs()
|
||||
|
||||
-- Cache party names
|
||||
UpdatePartyNames()
|
||||
|
||||
-- Polling frame for periodic threat updates
|
||||
self.pollFrame = CreateFrame("Frame", "RelationshipsThreatPlatesPollFrame")
|
||||
self.pollFrame.elapsed = 0
|
||||
self.pollFrame:SetScript("OnUpdate", function()
|
||||
this.elapsed = this.elapsed + arg1
|
||||
if this.elapsed >= THROTTLE then
|
||||
this.elapsed = 0
|
||||
addon.Threat:PollThreat()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Event frame for threat-related events
|
||||
local eventFrame = CreateFrame("Frame", "RelationshipsThreatPlatesEventFrame")
|
||||
eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
|
||||
eventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
|
||||
|
||||
-- Safely register threat event only if API exists
|
||||
if hasUnitThreatSituation or hasUnitDetailedThreatSituation then
|
||||
eventFrame:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE")
|
||||
end
|
||||
|
||||
eventFrame:SetScript("OnEvent", function()
|
||||
addon.Threat:OnEvent(event)
|
||||
end)
|
||||
|
||||
self.eventFrame = eventFrame
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Event handler
|
||||
-- ==================================================================
|
||||
function addon.Threat:OnEvent(ev)
|
||||
if ev == "PLAYER_TARGET_CHANGED" then
|
||||
self:PollThreat()
|
||||
elseif ev == "UNIT_THREAT_SITUATION_UPDATE" then
|
||||
local unit = arg1
|
||||
if unit and UnitExists(unit) then
|
||||
self:UpdateUnitThreat(unit)
|
||||
end
|
||||
elseif ev == "PARTY_MEMBERS_CHANGED" or ev == "RAID_ROSTER_UPDATE" then
|
||||
UpdatePartyNames()
|
||||
self:PollThreat()
|
||||
end
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Poll threat for all visible nameplates
|
||||
-- ==================================================================
|
||||
function addon.Threat:PollThreat()
|
||||
if not RelationshipsThreatPlatesDB or not RelationshipsThreatPlatesDB.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
for nameplate, data in pairs(addon.Nameplates.plates) do
|
||||
if nameplate:IsVisible() and data.unit then
|
||||
self:UpdateUnitThreat(data.unit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Get threat percentage for a unit
|
||||
-- Returns: threatPct (0-100), threatSit (0-3)
|
||||
--
|
||||
-- PRIORITY:
|
||||
-- 1. Group-wide scan via UnitDetailedThreatSituation (Turtle WoW)
|
||||
-- 2. Group-wide scan via GetThreatPercent API (custom extension)
|
||||
-- 3. Group-wide scan via UnitThreatSituation (partial API)
|
||||
-- 4. ThreatEngine combat log data (precise per-1% values)
|
||||
-- 5. Target-based heuristic (crude fallback only)
|
||||
--
|
||||
-- KEY CHANGE: Methods 1-3 now check ALL party/raid members' threat
|
||||
-- on the mob, not just the player. This ensures that:
|
||||
-- - Mobs attacking the player show the player's threat %
|
||||
-- - Mobs attacking party members show those members' threat %
|
||||
-- - ALL mobs in combat with the group show accurate threat bars
|
||||
----------------------------------------------------------------------
|
||||
|
||||
function addon.Threat:GetThreatPercent(unit)
|
||||
if not unit or not UnitExists(unit) then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- ==============================================================
|
||||
-- Pre-filter: Only check threat for hostile, in-combat mobs
|
||||
-- These checks apply regardless of which method we use below.
|
||||
-- ==============================================================
|
||||
|
||||
-- Only check threat for hostile units
|
||||
local reaction = UnitReaction(unit, "player")
|
||||
if reaction and reaction > 4 then
|
||||
-- Friendly unit - no threat display
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- Check if player OR any group member is in combat.
|
||||
-- CRITICAL FIX: Previously only checked UnitAffectingCombat("player"),
|
||||
-- which meant mobs attacking party members showed no threat when the
|
||||
-- player wasn't in combat. Now we check the entire group.
|
||||
local playerInCombat = UnitAffectingCombat("player")
|
||||
local groupInCombat = addon.ThreatEngine:IsGroupInCombat()
|
||||
|
||||
-- The MOB itself must be in combat.
|
||||
local ok_mc, mobCombat = pcall(UnitAffectingCombat, unit)
|
||||
if ok_mc and not mobCombat then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- Verify this is a hostile unit the player can attack
|
||||
local ok_ca, canAttack = pcall(UnitCanAttack, "player", unit)
|
||||
if ok_ca and not canAttack then
|
||||
-- Also check if any group member can attack it
|
||||
local groupCanAttack = false
|
||||
for i = 1, table.getn(partyUnits) do
|
||||
local ok_gca, gca = pcall(UnitCanAttack, partyUnits[i], unit)
|
||||
if ok_gca and gca then
|
||||
groupCanAttack = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not groupCanAttack then
|
||||
return 0, 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Get mob name and who the mob is targeting (used by multiple methods)
|
||||
local ok_name, mobName = pcall(UnitName, unit)
|
||||
if not ok_name or not mobName then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
local mobTargetName = nil
|
||||
local ok_mt, mt = pcall(UnitName, unit .. "target")
|
||||
if ok_mt and mt then
|
||||
mobTargetName = mt
|
||||
end
|
||||
|
||||
-- ==============================================================
|
||||
-- Methods 1-3: Group-wide native threat API scan
|
||||
-- Check ALL party/raid members' threat on this mob.
|
||||
-- This is the KEY fix: instead of only checking the player's
|
||||
-- threat, we check every group member and return the best.
|
||||
-- ==============================================================
|
||||
|
||||
if hasUnitDetailedThreatSituation or hasGetThreatPercent or hasUnitThreatSituation then
|
||||
local bestPct, bestSit, bestSource, playerPct, playerSit =
|
||||
ScanGroupThreatOnUnit(unit)
|
||||
|
||||
if bestPct and bestPct > 0 then
|
||||
-- We have threat data from at least one group member.
|
||||
|
||||
-- Determine whose threat to display:
|
||||
-- If the mob is targeting the player -> show player's threat
|
||||
-- If the mob is targeting a group member -> show that member's threat
|
||||
-- If the mob is targeting an outsider -> show player's threat
|
||||
-- Otherwise -> show the best (highest) threat from any member
|
||||
|
||||
if mobTargetName == playerName then
|
||||
-- Mob is attacking the player - show player's own threat
|
||||
return playerPct, playerSit
|
||||
elseif mobTargetName and partyNames[mobTargetName] then
|
||||
-- Mob is attacking a group member - show that member's threat
|
||||
-- Look up that member's threat from the group scan
|
||||
local memberUnit = partyNames[mobTargetName]
|
||||
local memberPct = nil
|
||||
local memberSit = nil
|
||||
|
||||
if hasUnitDetailedThreatSituation then
|
||||
local _, status, _, rawPct = SafeCall5(UnitDetailedThreatSituation, memberUnit, unit)
|
||||
if rawPct then
|
||||
memberPct = math.min(100, math.max(0, rawPct))
|
||||
memberSit = status or 0
|
||||
end
|
||||
end
|
||||
|
||||
if not memberPct and hasGetThreatPercent then
|
||||
local pct = SafeCall2(GetThreatPercent, memberUnit, unit)
|
||||
if pct then
|
||||
memberPct = math.min(100, math.max(0, pct))
|
||||
if hasUnitThreatSituation then
|
||||
memberSit = SafeCall2(UnitThreatSituation, memberUnit, unit) or 0
|
||||
else
|
||||
if memberPct >= 100 then memberSit = 3
|
||||
elseif memberPct >= 60 then memberSit = 2
|
||||
elseif memberPct >= 30 then memberSit = 1
|
||||
else memberSit = 0 end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not memberPct and hasUnitThreatSituation then
|
||||
memberSit = SafeCall2(UnitThreatSituation, memberUnit, unit) or 0
|
||||
if memberSit == 0 then memberPct = 0
|
||||
elseif memberSit == 1 then memberPct = 25
|
||||
elseif memberSit == 2 then memberPct = 60
|
||||
elseif memberSit == 3 then memberPct = 100 end
|
||||
end
|
||||
|
||||
if memberPct then
|
||||
return memberPct, memberSit or 0
|
||||
end
|
||||
|
||||
-- Fallback: can't get specific member data, use best
|
||||
return bestPct, bestSit
|
||||
else
|
||||
-- Mob targets an outsider, or we can't see its target.
|
||||
-- Show player's threat if they have any, otherwise the
|
||||
-- best group threat.
|
||||
if playerPct > 0 then
|
||||
return playerPct, playerSit
|
||||
end
|
||||
return bestPct, bestSit
|
||||
end
|
||||
end
|
||||
|
||||
-- All group members have 0 threat on this mob via native APIs,
|
||||
-- but the mob IS in combat. This can happen if:
|
||||
-- - Only ThreatEngine has data (combat log parsing)
|
||||
-- - The mob is fighting an outsider
|
||||
-- - The mob was just pulled and the API hasn't updated yet
|
||||
-- Fall through to ThreatEngine / heuristic below.
|
||||
end
|
||||
|
||||
-- ==============================================================
|
||||
-- Method 4: ThreatEngine combat log parsing
|
||||
-- This provides precise per-1% threat values based on actual
|
||||
-- damage/healing/ability threat tracked from the combat log.
|
||||
-- This is the PRIMARY method on servers without native APIs.
|
||||
--
|
||||
-- KEY FIX: Now uses group combat state instead of only player combat.
|
||||
-- If any group member is in combat, ThreatEngine data is relevant.
|
||||
-- ==============================================================
|
||||
|
||||
-- Someone in the group must be in combat for ThreatEngine data
|
||||
-- to be relevant. This prevents threat bars on idle mobs when
|
||||
-- no one in the group is fighting.
|
||||
if not groupInCombat then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- If the mob targets an outsider (someone not in the
|
||||
-- player's party/raid), verify the player has actually engaged
|
||||
-- this mob personally before showing any threat bar.
|
||||
if mobTargetName and mobTargetName ~= playerName and not partyNames[mobTargetName] then
|
||||
-- Mob targets someone outside our group.
|
||||
-- Check if the player has personally hit this mob.
|
||||
-- If player has 0 threat on this mob, don't show a bar.
|
||||
local playerThreat = addon.ThreatEngine:GetPlayerThreat(mobName)
|
||||
if playerThreat <= 0 then
|
||||
return 0, 0
|
||||
end
|
||||
-- Player has some threat but mob targets an outsider.
|
||||
-- The player's threat is secondary - show it but reduced.
|
||||
-- We can't know the outsider's threat, so estimate conservatively.
|
||||
-- Use ThreatEngine data if available, otherwise heuristic.
|
||||
local enginePct, engineSit, _, _, hasGroupData = addon.ThreatEngine:GetThreatOnTarget(mobName)
|
||||
if enginePct > 0 then
|
||||
if hasGroupData then
|
||||
return enginePct, engineSit
|
||||
end
|
||||
-- Solo data only - the outsider probably has more threat.
|
||||
-- Cap our displayed percentage conservatively.
|
||||
if enginePct >= 100 then
|
||||
return 80, 2
|
||||
end
|
||||
return enginePct, engineSit
|
||||
end
|
||||
-- No ThreatEngine data but player is hitting it - rough estimate
|
||||
if UnitIsUnit("target", unit) then
|
||||
return 40, 1
|
||||
end
|
||||
return 20, 0
|
||||
end
|
||||
|
||||
-- Query ThreatEngine with the mob's name
|
||||
local enginePct, engineSit, enginePlayerThreat, engineTopThreat, hasGroupData =
|
||||
addon.ThreatEngine:GetThreatOnTarget(mobName)
|
||||
|
||||
if enginePct > 0 then
|
||||
if hasGroupData then
|
||||
-- We have threat data from multiple sources (group broadcasts).
|
||||
-- The percentage is reliable - use it directly.
|
||||
return enginePct, engineSit
|
||||
end
|
||||
|
||||
-- ThreatEngine only has the player's own threat data (no group
|
||||
-- broadcasts). Cross-reference with mob's target to calibrate.
|
||||
if mobTargetName then
|
||||
if mobTargetName == playerName then
|
||||
-- Mob targets the player AND player has threat data.
|
||||
-- Player really does have aggro. Since we only know our
|
||||
-- own threat and we're the top, show as 100%.
|
||||
return 100, 3
|
||||
elseif partyNames[mobTargetName] then
|
||||
-- Mob targets a PARTY/RAID member. The player doesn't
|
||||
-- have aggro but does have some threat. Show the
|
||||
-- ThreatEngine percentage as the player's relative threat.
|
||||
-- This is much better than the old fixed 60%/30% estimate.
|
||||
return enginePct, engineSit
|
||||
else
|
||||
-- Mob targets an outsider - show conservative estimate
|
||||
if enginePct >= 100 then
|
||||
return 90, 2
|
||||
end
|
||||
return enginePct, engineSit
|
||||
end
|
||||
else
|
||||
-- Can't see mob's target. Show raw ThreatEngine data
|
||||
-- but cap at 90% since we can't confirm we have aggro.
|
||||
if enginePct >= 100 then
|
||||
return 90, 2
|
||||
end
|
||||
return enginePct, engineSit
|
||||
end
|
||||
end
|
||||
|
||||
-- ==============================================================
|
||||
-- Method 5: Target-based heuristic (last resort fallback)
|
||||
-- Only used when ThreatEngine has NO data for a mob.
|
||||
-- This can happen for: newly engaged mobs, mobs in combat with
|
||||
-- other players outside our group, or very brief encounters.
|
||||
--
|
||||
-- This is the MOST RELIABLE heuristic in Vanilla WoW:
|
||||
-- Who is the mob targeting? That person has aggro.
|
||||
--
|
||||
-- KEY CHANGE: Now properly handles party/raid members.
|
||||
-- If the mob is targeting a group member, we show a high
|
||||
-- threat bar (that member has aggro), not a low estimate.
|
||||
-- ==============================================================
|
||||
|
||||
-- Check if the mob is targeting someone
|
||||
if mobTargetName then
|
||||
if mobTargetName == playerName then
|
||||
-- Mob is targeting the PLAYER - player has AGGRO (100%)
|
||||
threatPct = 100
|
||||
situation = 3
|
||||
elseif partyNames[mobTargetName] then
|
||||
-- Mob is targeting a PARTY/RAID member.
|
||||
-- That member has aggro - show high threat.
|
||||
-- This is the KEY fix: previously showed only 60%/30%
|
||||
-- for mobs targeting group members. Now shows 100%
|
||||
-- because that group member IS the one being attacked.
|
||||
threatPct = 100
|
||||
situation = 3
|
||||
else
|
||||
-- Mob is targeting someone outside the party
|
||||
-- (another player, a pet, etc.)
|
||||
if UnitIsUnit("target", unit) then
|
||||
threatPct = 40
|
||||
situation = 1
|
||||
else
|
||||
threatPct = 15
|
||||
situation = 0
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Mob's target is nil. We already know it's in combat.
|
||||
-- This happens when someone outside our group has aggro
|
||||
-- or the mob is between targets.
|
||||
if UnitIsUnit("target", unit) then
|
||||
threatPct = 40
|
||||
situation = 1
|
||||
else
|
||||
threatPct = 10
|
||||
situation = 0
|
||||
end
|
||||
end
|
||||
|
||||
return threatPct, situation
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Update threat for a specific unit and notify nameplates
|
||||
-- ==================================================================
|
||||
function addon.Threat:UpdateUnitThreat(unit)
|
||||
if not unit or not UnitExists(unit) then return end
|
||||
|
||||
local pct, situation = self:GetThreatPercent(unit)
|
||||
local uid = addon:GetUnitID(unit)
|
||||
|
||||
if uid then
|
||||
-- Get mob target info for group context
|
||||
local mobTargetName = nil
|
||||
local ok_mt, mt = pcall(UnitName, unit .. "target")
|
||||
if ok_mt and mt then
|
||||
mobTargetName = mt
|
||||
end
|
||||
|
||||
threatCache[uid] = {
|
||||
pct = pct,
|
||||
situation = situation,
|
||||
unit = unit,
|
||||
lastUpdate = GetTime(),
|
||||
mobTargetName = mobTargetName,
|
||||
}
|
||||
|
||||
-- Also store in group threat cache for cross-reference
|
||||
groupThreatCache[uid] = {
|
||||
bestPct = pct,
|
||||
bestSit = situation,
|
||||
mobTargetName = mobTargetName,
|
||||
lastUpdate = GetTime(),
|
||||
}
|
||||
|
||||
addon.Nameplates:UpdateThreatForUnit(unit, pct, situation)
|
||||
end
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Get cached threat for a unit identifier
|
||||
-- ==================================================================
|
||||
function addon.Threat:GetCachedThreat(uid)
|
||||
if not uid then return 0, 0 end
|
||||
local cache = threatCache[uid]
|
||||
if cache then
|
||||
if GetTime() - cache.lastUpdate < 5 then
|
||||
return cache.pct, cache.situation
|
||||
end
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Get threat percentage by mob NAME (no unit token required)
|
||||
-- This is the fallback for when FindUnitByName couldn't resolve
|
||||
-- a unit token for a nameplate.
|
||||
--
|
||||
-- CRITICAL FIX for same-name mob confusion:
|
||||
-- In Vanilla 1.12 there is no UnitGUID. Multiple mobs with the
|
||||
-- same name (e.g. "Defias Bandit") all share one ThreatEngine
|
||||
-- entry. If we return threat data by name alone, idle same-name
|
||||
-- mobs will incorrectly show threat bars.
|
||||
--
|
||||
-- Solution: GetThreatByName now REQUIRES finding a unit token
|
||||
-- that is verified to be in combat. If no combat-verified unit
|
||||
-- token can be found for the given name, it returns 0, 0.
|
||||
-- This ensures idle mobs never get threat data.
|
||||
--
|
||||
-- Returns: threatPct (0-100), threatSit (0-3)
|
||||
-- ==================================================================
|
||||
function addon.Threat:GetThreatByName(mobName)
|
||||
if not mobName or strlen(mobName) == 0 then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- KEY FIX: Only require that SOMEONE in the group is in combat,
|
||||
-- not necessarily the player. This allows threat bars to show
|
||||
-- on mobs attacking party/raid members even when the player
|
||||
-- isn't in combat themselves.
|
||||
if not addon.ThreatEngine:IsGroupInCombat() then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- ==============================================================
|
||||
-- Step 1: Try to find a combat-verified unit token.
|
||||
-- If we can find one and verify it's in combat, use the full
|
||||
-- GetThreatPercent which has all the proper checks.
|
||||
-- ==============================================================
|
||||
|
||||
local unitTokens = {
|
||||
"target", "mouseover", "targettarget", "pet",
|
||||
}
|
||||
|
||||
for i = 1, 4 do
|
||||
unitTokens[table.getn(unitTokens) + 1] = "party" .. i .. "target"
|
||||
unitTokens[table.getn(unitTokens) + 1] = "party" .. i .. "targettarget"
|
||||
end
|
||||
|
||||
for i = 1, 40 do
|
||||
unitTokens[table.getn(unitTokens) + 1] = "raid" .. i .. "target"
|
||||
unitTokens[table.getn(unitTokens) + 1] = "raid" .. i .. "targettarget"
|
||||
end
|
||||
|
||||
-- Search for a unit token that matches the name AND is in combat
|
||||
for i = 1, table.getn(unitTokens) do
|
||||
local token = unitTokens[i]
|
||||
local ok, exists = pcall(UnitExists, token)
|
||||
if ok and exists then
|
||||
local ok2, name = pcall(UnitName, token)
|
||||
if ok2 and name and name == mobName then
|
||||
local ok3, inCombat = pcall(UnitAffectingCombat, token)
|
||||
if ok3 and inCombat then
|
||||
local pct, sit = self:GetThreatPercent(token)
|
||||
return pct, sit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ==============================================================
|
||||
-- Step 2: No combat-verified unit token found.
|
||||
--
|
||||
-- For UNIQUE-name mobs (only 1 plate with this name visible),
|
||||
-- the ThreatEngine data IS unambiguous — there's only one mob
|
||||
-- with this name, so the threat data is definitely about it.
|
||||
-- We can use ThreatEngine data directly.
|
||||
--
|
||||
-- For SAME-name mobs (multiple plates share this name), we
|
||||
-- CANNOT use ThreatEngine data without a unit token because
|
||||
-- it would apply the same threat to all same-name plates,
|
||||
-- including idle ones.
|
||||
--
|
||||
-- The caller (UpdateAllPlates) decides whether a name is
|
||||
-- unique. GetThreatByName is ONLY called for unique names
|
||||
-- from UpdateAllPlates, so it's safe to use ThreatEngine here.
|
||||
-- ==============================================================
|
||||
|
||||
if addon.ThreatEngine:HasDataForTarget(mobName) then
|
||||
local enginePct, engineSit, _, _, hasGroupData =
|
||||
addon.ThreatEngine:GetThreatOnTarget(mobName)
|
||||
|
||||
if enginePct > 0 then
|
||||
-- We have ThreatEngine data for this unique-name mob.
|
||||
-- Cap at 90% since we can't verify who has aggro
|
||||
-- without seeing the mob's target.
|
||||
if enginePct >= 100 then
|
||||
return 90, 2
|
||||
end
|
||||
return enginePct, engineSit
|
||||
end
|
||||
end
|
||||
|
||||
-- No ThreatEngine data either. This mob likely hasn't been
|
||||
-- engaged by the player. Return 0.
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Check if a mob is attacking any group member
|
||||
-- Returns: the name of the group member being attacked, or nil
|
||||
-- ==================================================================
|
||||
function addon.Threat:GetMobTarget(mobUnit)
|
||||
if not mobUnit or not UnitExists(mobUnit) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ok, targetName = pcall(UnitName, mobUnit .. "target")
|
||||
if ok and targetName then
|
||||
if targetName == playerName or partyNames[targetName] then
|
||||
return targetName
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Get cached group threat for a unit identifier
|
||||
-- Returns: bestPct, bestSit, mobTargetName
|
||||
-- ==================================================================
|
||||
function addon.Threat:GetGroupCachedThreat(uid)
|
||||
if not uid then return 0, 0, nil end
|
||||
local cache = groupThreatCache[uid]
|
||||
if cache then
|
||||
if GetTime() - cache.lastUpdate < 5 then
|
||||
return cache.bestPct, cache.bestSit, cache.mobTargetName
|
||||
end
|
||||
end
|
||||
return 0, 0, nil
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Clear all threat data
|
||||
-- ==================================================================
|
||||
function addon.Threat:ClearAll()
|
||||
threatCache = {}
|
||||
groupThreatCache = {}
|
||||
end
|
||||
Reference in New Issue
Block a user