调整dps插件对仇恨的估算方式
优化dps插件
This commit is contained in:
863
ThreatEngine.lua
Normal file
863
ThreatEngine.lua
Normal file
@@ -0,0 +1,863 @@
|
||||
local NanamiDPS = NanamiDPS
|
||||
local TC = NanamiDPS.ThreatCoefficients
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- ThreatEngine: Adaptive dual-track hybrid threat monitoring
|
||||
--
|
||||
-- Track 1 (Primary): TWThreat server API — direct authoritative data for
|
||||
-- Elite/Boss targets when in party/raid. Completely bypasses combat
|
||||
-- log parsing for the queried target.
|
||||
--
|
||||
-- Track 2 (Fallback): Local heuristic combat log estimation — used for
|
||||
-- normal mobs, solo play, or environments without SuperWoW.
|
||||
--
|
||||
-- Data model: 3D table [targetKey][playerName] = { threat, tps, ... }
|
||||
-- targetKey = GUID (TWThreat) or RaidIcon/virtualID (fallback)
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local TE = CreateFrame("Frame", "NanamiDPSThreatEngine", UIParent)
|
||||
NanamiDPS.ThreatEngine = TE
|
||||
|
||||
local _find = string.find
|
||||
local _sub = string.sub
|
||||
local _len = string.len
|
||||
local _lower = string.lower
|
||||
local _floor = math.floor
|
||||
local _max = math.max
|
||||
local _pairs = pairs
|
||||
local _tonumber = tonumber
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Constants
|
||||
-------------------------------------------------------------------------------
|
||||
local QUERY_INTERVAL_DEFAULT = 0.5
|
||||
local QUERY_PREFIX = "TWT_UDTSv4"
|
||||
local QUERY_PREFIX_TM = "TWT_UDTSv4_TM"
|
||||
local API_MARKER = "TWTv4="
|
||||
local TM_MARKER = "TMTv1="
|
||||
local ADDON_PREFIX = "TWT"
|
||||
local HISTORY_WINDOW = 10
|
||||
local TPS_WINDOW = 5
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- State
|
||||
-------------------------------------------------------------------------------
|
||||
TE.targets = {}
|
||||
TE.apiActive = false
|
||||
TE.apiTargetKey = nil
|
||||
TE.inCombat = false
|
||||
TE.tankMode = false
|
||||
|
||||
TE.playerName = UnitName("player")
|
||||
local _, playerClassToken = UnitClass("player")
|
||||
TE.playerClass = playerClassToken
|
||||
|
||||
TE.talentCache = {}
|
||||
TE.stanceModCache = {}
|
||||
|
||||
TE.queryTimer = 0
|
||||
TE.queryInterval = QUERY_INTERVAL_DEFAULT
|
||||
|
||||
TE.lastUpdateTime = 0
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- String split utility (1.12 compatible)
|
||||
-------------------------------------------------------------------------------
|
||||
local function strsplit(sep, str)
|
||||
local t = {}
|
||||
local pos = 1
|
||||
while true do
|
||||
local s, e = _find(str, sep, pos, true)
|
||||
if not s then
|
||||
local piece = _sub(str, pos)
|
||||
if piece ~= "" then t[table.getn(t) + 1] = piece end
|
||||
break
|
||||
end
|
||||
local piece = _sub(str, pos, s - 1)
|
||||
t[table.getn(t) + 1] = piece
|
||||
pos = e + 1
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Target key helpers
|
||||
-------------------------------------------------------------------------------
|
||||
local function GetTargetKey(unit)
|
||||
if not unit or not UnitExists(unit) then return nil end
|
||||
if UnitGUID then
|
||||
local guid = UnitGUID(unit)
|
||||
if guid then return "G:" .. guid end
|
||||
end
|
||||
local icon = GetRaidTargetIndex and GetRaidTargetIndex(unit)
|
||||
if icon then return "I:" .. icon end
|
||||
local name = UnitName(unit)
|
||||
local hp = UnitHealth(unit)
|
||||
local hpMax = UnitHealthMax(unit)
|
||||
if name then return "V:" .. name .. ":" .. (hpMax or 0) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function IsEliteOrBoss(unit)
|
||||
if not unit or not UnitExists(unit) then return false end
|
||||
local classification = UnitClassification and UnitClassification(unit)
|
||||
if classification == "worldboss" or classification == "rareelite" or classification == "elite" then
|
||||
return true
|
||||
end
|
||||
local level = UnitLevel(unit)
|
||||
if level == -1 then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
local function IsInGroup()
|
||||
return (GetNumRaidMembers() > 0) or (GetNumPartyMembers() > 0)
|
||||
end
|
||||
|
||||
local function GetChannel()
|
||||
if GetNumRaidMembers() > 0 then return "RAID" end
|
||||
if GetNumPartyMembers() > 0 then return "PARTY" end
|
||||
return nil
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Talent scanning: cached at combat start
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:ScanTalents()
|
||||
self.talentCache = {}
|
||||
local class = self.playerClass
|
||||
local talentDefs = TC.talentMultipliers[class]
|
||||
if not talentDefs then return end
|
||||
|
||||
for _, def in _pairs(talentDefs) do
|
||||
local _, _, _, _, rank = GetTalentInfo(def.tab, def.index)
|
||||
rank = rank or 0
|
||||
self.talentCache[def.label] = {
|
||||
rank = rank,
|
||||
mod = def.base + rank * def.perPoint,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function TE:GetTalentMod()
|
||||
local total = 0
|
||||
for _, data in _pairs(self.talentCache) do
|
||||
total = total + data.mod
|
||||
end
|
||||
return 1.0 + total
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Stance / buff scanning for threat modifier
|
||||
-------------------------------------------------------------------------------
|
||||
local STANCE_CACHE_TTL = 2
|
||||
|
||||
function TE:ScanUnitThreatMod(unit)
|
||||
if not unit or not UnitExists(unit) then return 1.0, 1.0, false, false end
|
||||
local stanceMod = 1.0
|
||||
local buffMod = 1.0
|
||||
local hasRF = false
|
||||
local hasRockbiter = false
|
||||
|
||||
for i = 1, 32 do
|
||||
local texture = UnitBuff(unit, i)
|
||||
if not texture then break end
|
||||
for _, s in _pairs(TC.stancePatterns) do
|
||||
if _find(texture, s.pat) then
|
||||
stanceMod = s.mod
|
||||
end
|
||||
end
|
||||
for _, b in _pairs(TC.buffPatterns) do
|
||||
if _find(texture, b.pat) then
|
||||
buffMod = buffMod * b.mod
|
||||
end
|
||||
end
|
||||
if TC.righteousFury and _find(texture, TC.righteousFury.texture) then
|
||||
hasRF = true
|
||||
end
|
||||
if TC.rockbiter and _find(texture, TC.rockbiter.texture) then
|
||||
hasRockbiter = true
|
||||
end
|
||||
end
|
||||
|
||||
return stanceMod, buffMod, hasRF, hasRockbiter
|
||||
end
|
||||
|
||||
function TE:GetUnitThreatMod(name, unit)
|
||||
local now = GetTime()
|
||||
local cached = self.stanceModCache[name]
|
||||
if cached and (now - cached.time) < STANCE_CACHE_TTL then
|
||||
return cached.stanceMod, cached.buffMod, cached.hasRF, cached.hasRockbiter
|
||||
end
|
||||
|
||||
if not unit then
|
||||
unit = NanamiDPS.Parser and NanamiDPS.Parser:UnitByName(name) or nil
|
||||
end
|
||||
|
||||
local sm, bm, rf, rb = self:ScanUnitThreatMod(unit)
|
||||
self.stanceModCache[name] = {
|
||||
stanceMod = sm, buffMod = bm, hasRF = rf, hasRockbiter = rb,
|
||||
time = now,
|
||||
}
|
||||
return sm, bm, rf, rb
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Track 2: Local heuristic threat calculation
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:CalculateLocalThreat(source, spell, damage, isHeal, sourceClass)
|
||||
if not damage or damage <= 0 then return 0 end
|
||||
|
||||
local threat = damage
|
||||
local coeff = spell and TC.spells[spell]
|
||||
|
||||
if isHeal then
|
||||
threat = damage * TC.HEAL_THREAT_COEFF
|
||||
elseif coeff then
|
||||
if coeff.zero then return 0 end
|
||||
threat = damage * (coeff.mult or 1.0) + (coeff.bonus or 0)
|
||||
end
|
||||
|
||||
local class = sourceClass
|
||||
if class and TC.classPassiveMod[class] then
|
||||
threat = threat * TC.classPassiveMod[class]
|
||||
end
|
||||
|
||||
local stanceMod, buffMod, hasRF, hasRockbiter = self:GetUnitThreatMod(source)
|
||||
threat = threat * stanceMod * buffMod
|
||||
|
||||
if hasRF and class == "PALADIN" then
|
||||
local rfMod = TC.righteousFury.baseMod
|
||||
local talentDef = TC.righteousFury
|
||||
if talentDef.talentTab then
|
||||
local _, _, _, _, rank = GetTalentInfo(talentDef.talentTab, talentDef.talentIndex)
|
||||
rank = rank or 0
|
||||
rfMod = rfMod + rank * talentDef.perPoint
|
||||
end
|
||||
threat = threat * rfMod
|
||||
end
|
||||
|
||||
if hasRockbiter and class == "SHAMAN" then
|
||||
threat = threat * TC.rockbiter.globalMod
|
||||
end
|
||||
|
||||
if class == "WARRIOR" or class == "DRUID" or class == "SHAMAN" then
|
||||
local talentMod = self:GetTalentMod()
|
||||
if talentMod > 1.0 then
|
||||
threat = threat * talentMod
|
||||
end
|
||||
end
|
||||
|
||||
return _max(threat, 0)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- 3D data model operations
|
||||
-------------------------------------------------------------------------------
|
||||
local function EnsureTarget(targetKey)
|
||||
if not TE.targets[targetKey] then
|
||||
TE.targets[targetKey] = {
|
||||
players = {},
|
||||
tankName = nil,
|
||||
tankThreat = 0,
|
||||
lastUpdate = GetTime(),
|
||||
source = "local",
|
||||
}
|
||||
end
|
||||
return TE.targets[targetKey]
|
||||
end
|
||||
|
||||
local function EnsurePlayer(targetData, playerName)
|
||||
if not targetData.players[playerName] then
|
||||
targetData.players[playerName] = {
|
||||
threat = 0,
|
||||
tps = 0,
|
||||
isTanking = false,
|
||||
isMelee = false,
|
||||
perc = 0,
|
||||
history = {},
|
||||
lastThreatTime = 0,
|
||||
}
|
||||
end
|
||||
return targetData.players[playerName]
|
||||
end
|
||||
|
||||
function TE:AddLocalThreat(targetKey, source, amount)
|
||||
if not targetKey or not source or not amount then return end
|
||||
|
||||
local td = EnsureTarget(targetKey)
|
||||
if td.source == "api" then return end
|
||||
|
||||
local pd = EnsurePlayer(td, source)
|
||||
pd.threat = pd.threat + amount
|
||||
pd.lastThreatTime = GetTime()
|
||||
|
||||
local now = GetTime()
|
||||
table.insert(pd.history, { time = now, threat = pd.threat })
|
||||
local cutoff = now - TPS_WINDOW
|
||||
while table.getn(pd.history) > 0 and pd.history[1].time < cutoff do
|
||||
table.remove(pd.history, 1)
|
||||
end
|
||||
|
||||
if table.getn(pd.history) >= 2 then
|
||||
local first = pd.history[1]
|
||||
local last = pd.history[table.getn(pd.history)]
|
||||
local dt = last.time - first.time
|
||||
if dt > 0 then
|
||||
pd.tps = (last.threat - first.threat) / dt
|
||||
end
|
||||
end
|
||||
|
||||
td.lastUpdate = now
|
||||
self:RecalcTankStatus(targetKey)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Taunt handling: copy highest threat to the taunter (Growl, Taunt, etc.)
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:HandleTaunt(taunterName, targetKey)
|
||||
if not targetKey then return end
|
||||
local td = self.targets[targetKey]
|
||||
if not td then return end
|
||||
|
||||
local maxThreat = 0
|
||||
for name, pd in _pairs(td.players) do
|
||||
if name ~= taunterName and pd.threat > maxThreat then
|
||||
maxThreat = pd.threat
|
||||
end
|
||||
end
|
||||
|
||||
if maxThreat > 0 then
|
||||
local pd = EnsurePlayer(td, taunterName)
|
||||
if pd.threat < maxThreat then
|
||||
pd.threat = maxThreat
|
||||
pd.isTanking = true
|
||||
pd.lastThreatTime = GetTime()
|
||||
end
|
||||
end
|
||||
|
||||
self:RecalcTankStatus(targetKey)
|
||||
end
|
||||
|
||||
function TE:WipePlayerThreat(source, wipeType)
|
||||
for targetKey, td in _pairs(self.targets) do
|
||||
local pd = td.players[source]
|
||||
if pd then
|
||||
if wipeType == "FULL_WIPE" then
|
||||
pd.threat = 0
|
||||
pd.tps = 0
|
||||
pd.history = {}
|
||||
elseif wipeType == "HALF_WIPE" then
|
||||
pd.threat = pd.threat * 0.5
|
||||
elseif wipeType == "TEMP_REDUCE" then
|
||||
pd.threat = pd.threat * 0.5
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TE:RecalcTankStatus(targetKey)
|
||||
local td = self.targets[targetKey]
|
||||
if not td then return end
|
||||
|
||||
local maxThreat = 0
|
||||
local tankName = nil
|
||||
for name, pd in _pairs(td.players) do
|
||||
if pd.threat > maxThreat then
|
||||
maxThreat = pd.threat
|
||||
tankName = name
|
||||
end
|
||||
end
|
||||
|
||||
td.tankName = tankName
|
||||
td.tankThreat = maxThreat
|
||||
|
||||
for name, pd in _pairs(td.players) do
|
||||
pd.isTanking = (name == tankName)
|
||||
pd.perc = maxThreat > 0 and (pd.threat / maxThreat * 100) or 0
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Track 1: TWThreat API — packet handling
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:HandleAPIPacket(rawData)
|
||||
local threatData = rawData
|
||||
local tmData = nil
|
||||
|
||||
local hashPos = _find(rawData, "#", 1, true)
|
||||
if hashPos then
|
||||
threatData = _sub(rawData, 1, hashPos - 1)
|
||||
tmData = _sub(rawData, hashPos + 1)
|
||||
end
|
||||
|
||||
local apiStart = _find(threatData, API_MARKER, 1, true)
|
||||
if not apiStart then return end
|
||||
|
||||
local playersStr = _sub(threatData, apiStart + _len(API_MARKER))
|
||||
local entries = strsplit(";", playersStr)
|
||||
|
||||
local targetKey = self.apiTargetKey or "api_target"
|
||||
local td = EnsureTarget(targetKey)
|
||||
td.source = "api"
|
||||
td.lastUpdate = GetTime()
|
||||
td.tankName = nil
|
||||
td.tankThreat = 0
|
||||
|
||||
local oldPlayers = td.players
|
||||
td.players = {}
|
||||
|
||||
for _, entry in _pairs(entries) do
|
||||
local fields = strsplit(":", entry)
|
||||
if fields[1] and fields[2] and fields[3] and fields[4] and fields[5] then
|
||||
local name = fields[1]
|
||||
local isTank = fields[2] == "1"
|
||||
local threat = _tonumber(fields[3]) or 0
|
||||
local perc = _tonumber(fields[4]) or 0
|
||||
local isMelee = fields[5] == "1"
|
||||
|
||||
local prevHistory = oldPlayers[name] and oldPlayers[name].history or {}
|
||||
|
||||
local pd = EnsurePlayer(td, name)
|
||||
pd.threat = threat
|
||||
pd.isTanking = isTank
|
||||
pd.perc = perc
|
||||
pd.isMelee = isMelee
|
||||
pd.lastThreatTime = GetTime()
|
||||
|
||||
table.insert(prevHistory, { time = GetTime(), threat = threat })
|
||||
local cutoff = GetTime() - TPS_WINDOW
|
||||
while table.getn(prevHistory) > 0 and prevHistory[1].time < cutoff do
|
||||
table.remove(prevHistory, 1)
|
||||
end
|
||||
pd.history = prevHistory
|
||||
|
||||
if table.getn(pd.history) >= 2 then
|
||||
local first = pd.history[1]
|
||||
local last = pd.history[table.getn(pd.history)]
|
||||
local dt = last.time - first.time
|
||||
if dt > 0 then
|
||||
pd.tps = (last.threat - first.threat) / dt
|
||||
end
|
||||
end
|
||||
|
||||
if isTank then
|
||||
td.tankName = name
|
||||
td.tankThreat = threat
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if tmData then
|
||||
self:HandleTankModePacket(tmData)
|
||||
end
|
||||
|
||||
self:FireUpdate()
|
||||
end
|
||||
|
||||
function TE:HandleTankModePacket(raw)
|
||||
local tmStart = _find(raw, TM_MARKER, 1, true)
|
||||
if not tmStart then return end
|
||||
|
||||
local dataStr = _sub(raw, tmStart + _len(TM_MARKER))
|
||||
local entries = strsplit(";", dataStr)
|
||||
|
||||
for _, entry in _pairs(entries) do
|
||||
local fields = strsplit(":", entry)
|
||||
if fields[1] and fields[2] and fields[3] and fields[4] then
|
||||
local creature = fields[1]
|
||||
local guid = fields[2]
|
||||
local name = fields[3]
|
||||
local perc = _tonumber(fields[4]) or 0
|
||||
|
||||
local targetKey = "G:" .. guid
|
||||
local td = EnsureTarget(targetKey)
|
||||
td.source = "api_tm"
|
||||
td.lastUpdate = GetTime()
|
||||
|
||||
local pd = EnsurePlayer(td, name)
|
||||
pd.perc = perc
|
||||
pd.isTanking = true
|
||||
td.tankName = name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- API query ticker
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:SendQuery()
|
||||
if not self.inCombat then return end
|
||||
if not IsInGroup() then return end
|
||||
|
||||
local unit = "target"
|
||||
if not UnitExists(unit) or UnitIsPlayer(unit) or not UnitAffectingCombat(unit) then
|
||||
return
|
||||
end
|
||||
|
||||
self.apiTargetKey = GetTargetKey(unit)
|
||||
|
||||
local channel = GetChannel()
|
||||
if not channel then return end
|
||||
|
||||
local prefix = self.tankMode and QUERY_PREFIX_TM or QUERY_PREFIX
|
||||
local limit = 10
|
||||
SendAddonMessage(prefix, "limit=" .. limit, channel)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Query result: OT analysis for current player
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:GetOTStatus(targetKey)
|
||||
local td = self.targets[targetKey]
|
||||
if not td then
|
||||
return { safe = true, pct = 0, threshold = 0, buffer = 0, tankName = nil }
|
||||
end
|
||||
|
||||
local myData = td.players[self.playerName]
|
||||
if not myData then
|
||||
return { safe = true, pct = 0, threshold = 0, buffer = 0, tankName = td.tankName }
|
||||
end
|
||||
|
||||
local tankThreat = td.tankThreat
|
||||
if tankThreat <= 0 then
|
||||
return { safe = true, pct = 0, threshold = 0, buffer = 0, tankName = td.tankName }
|
||||
end
|
||||
|
||||
local threshold = myData.isMelee and TC.OT_MELEE_THRESHOLD or TC.OT_RANGED_THRESHOLD
|
||||
local otPoint = tankThreat * threshold
|
||||
local myThreat = myData.threat
|
||||
local pct = myThreat / otPoint * 100
|
||||
local buffer = otPoint - myThreat
|
||||
|
||||
return {
|
||||
safe = myThreat < otPoint,
|
||||
pct = pct,
|
||||
threshold = threshold,
|
||||
otPoint = otPoint,
|
||||
buffer = buffer,
|
||||
myThreat = myThreat,
|
||||
tankThreat = tankThreat,
|
||||
tankName = td.tankName,
|
||||
isMelee = myData.isMelee,
|
||||
}
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Get sorted threat list for display
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:GetThreatList(targetKey)
|
||||
local td = self.targets[targetKey]
|
||||
if not td then return {} end
|
||||
|
||||
local list = {}
|
||||
for name, pd in _pairs(td.players) do
|
||||
table.insert(list, {
|
||||
name = name,
|
||||
threat = pd.threat,
|
||||
tps = pd.tps,
|
||||
perc = pd.perc,
|
||||
isTanking = pd.isTanking,
|
||||
isMelee = pd.isMelee,
|
||||
})
|
||||
end
|
||||
|
||||
table.sort(list, function(a, b) return a.threat > b.threat end)
|
||||
|
||||
local top = list[1] and list[1].threat or 0
|
||||
for _, entry in _pairs(list) do
|
||||
entry.relativePercent = top > 0 and (entry.threat / top * 100) or 0
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
function TE:GetActiveTargetKey()
|
||||
if self.apiActive and self.apiTargetKey then
|
||||
return self.apiTargetKey
|
||||
end
|
||||
return GetTargetKey("target")
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Cleanup stale data
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:PurgeStaleTargets()
|
||||
local now = GetTime()
|
||||
local staleThreshold = 30
|
||||
local keysToRemove = {}
|
||||
for key, td in _pairs(self.targets) do
|
||||
if (now - td.lastUpdate) > staleThreshold then
|
||||
keysToRemove[table.getn(keysToRemove) + 1] = key
|
||||
end
|
||||
end
|
||||
for _, key in _pairs(keysToRemove) do
|
||||
self.targets[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Refresh callback
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:FireUpdate()
|
||||
NanamiDPS:FireCallback("threat_update")
|
||||
self.lastUpdateTime = GetTime()
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Event registration & main loop
|
||||
-------------------------------------------------------------------------------
|
||||
TE:RegisterEvent("CHAT_MSG_ADDON")
|
||||
TE:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||
TE:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
TE:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
|
||||
TE:SetScript("OnEvent", function()
|
||||
if event == "CHAT_MSG_ADDON" then
|
||||
if arg2 and _find(arg2, API_MARKER, 1, true) then
|
||||
TE:HandleAPIPacket(arg2)
|
||||
TE.apiActive = true
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_REGEN_DISABLED" then
|
||||
TE.inCombat = true
|
||||
TE:ScanTalents()
|
||||
TE.stanceModCache = {}
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_REGEN_ENABLED" then
|
||||
TE.inCombat = false
|
||||
TE.apiActive = false
|
||||
TE.apiTargetKey = nil
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_TARGET_CHANGED" then
|
||||
TE.apiTargetKey = GetTargetKey("target")
|
||||
if TE.inCombat and UnitExists("target") and not UnitIsPlayer("target") then
|
||||
local td = TE.targets[TE.apiTargetKey]
|
||||
if not td or td.source ~= "api" then
|
||||
TE.apiActive = false
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
end)
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Threat-clearing buff detection via direct buff scanning
|
||||
-- More reliable than combat log text matching for abilities like
|
||||
-- Feign Death, Vanish, etc.
|
||||
-------------------------------------------------------------------------------
|
||||
TE.threatWipeBuffs = {
|
||||
{ pat = "Ability_Rogue_FeignDeath", wipe = "FULL_WIPE" },
|
||||
{ pat = "FeignDeath", wipe = "FULL_WIPE" },
|
||||
{ pat = "Ability_Vanish", wipe = "FULL_WIPE" },
|
||||
{ pat = "Spell_Shadow_Possession", wipe = "TEMP_REDUCE" },
|
||||
{ pat = "Spell_Holy_FadeAway", wipe = "TEMP_REDUCE" },
|
||||
}
|
||||
TE.lastWipeCheck = 0
|
||||
TE.wipeActive = {}
|
||||
|
||||
function TE:CheckThreatWipeBuffs()
|
||||
local now = GetTime()
|
||||
if (now - self.lastWipeCheck) < 0.3 then return end
|
||||
self.lastWipeCheck = now
|
||||
|
||||
local unit = "player"
|
||||
if not UnitExists(unit) then return end
|
||||
|
||||
for _, def in _pairs(self.threatWipeBuffs) do
|
||||
local found = false
|
||||
for i = 1, 32 do
|
||||
local tex = UnitBuff(unit, i)
|
||||
if not tex then break end
|
||||
if _find(tex, def.pat) then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if found and not self.wipeActive[def.pat] then
|
||||
self.wipeActive[def.pat] = true
|
||||
self:WipePlayerThreat(self.playerName, def.wipe)
|
||||
|
||||
local DS = NanamiDPS and NanamiDPS.DataStore
|
||||
if DS then
|
||||
local current = DS:GetPlayerThreat(self.playerName)
|
||||
if current > 0 then
|
||||
DS:AddThreat(self.playerName, -current)
|
||||
end
|
||||
end
|
||||
elseif not found and self.wipeActive[def.pat] then
|
||||
self.wipeActive[def.pat] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- targettarget heuristic: if the mob is attacking entity X, then X must
|
||||
-- have the highest threat. This corrects for pet Growl and other taunt
|
||||
-- effects that don't produce combat log damage.
|
||||
-------------------------------------------------------------------------------
|
||||
TE.lastToTCheck = 0
|
||||
local TOT_CHECK_INTERVAL = 0.5
|
||||
|
||||
function TE:CheckTargetOfTarget()
|
||||
local now = GetTime()
|
||||
if (now - self.lastToTCheck) < TOT_CHECK_INTERVAL then return end
|
||||
self.lastToTCheck = now
|
||||
|
||||
if not UnitExists("target") or UnitIsPlayer("target") then return end
|
||||
if not UnitExists("targettarget") then return end
|
||||
|
||||
local targetKey = self:GetCurrentTargetKey()
|
||||
if not targetKey then return end
|
||||
if self:IsAPIActiveForTarget(targetKey) then return end
|
||||
|
||||
local td = self.targets[targetKey]
|
||||
if not td then return end
|
||||
|
||||
local totName = UnitName("targettarget")
|
||||
if not totName then return end
|
||||
|
||||
local totData = td.players[totName]
|
||||
if not totData then
|
||||
for pName, _ in _pairs(td.players) do
|
||||
if _find(pName, totName, 1, true) then
|
||||
totData = td.players[pName]
|
||||
totName = pName
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not totData then
|
||||
totData = EnsurePlayer(td, totName)
|
||||
end
|
||||
|
||||
local maxOther = 0
|
||||
for name, pd in _pairs(td.players) do
|
||||
if name ~= totName and pd.threat > maxOther then
|
||||
maxOther = pd.threat
|
||||
end
|
||||
end
|
||||
|
||||
if totData.threat < maxOther then
|
||||
totData.threat = maxOther
|
||||
totData.isTanking = true
|
||||
totData.lastThreatTime = now
|
||||
self:RecalcTankStatus(targetKey)
|
||||
elseif not totData.isTanking then
|
||||
totData.isTanking = true
|
||||
self:RecalcTankStatus(targetKey)
|
||||
end
|
||||
end
|
||||
|
||||
TE:SetScript("OnUpdate", function()
|
||||
if not TE.inCombat then return end
|
||||
|
||||
local now = GetTime()
|
||||
|
||||
TE:CheckThreatWipeBuffs()
|
||||
TE:CheckTargetOfTarget()
|
||||
|
||||
if (now - TE.queryTimer) >= TE.queryInterval then
|
||||
TE.queryTimer = now
|
||||
TE:SendQuery()
|
||||
end
|
||||
|
||||
if (now - TE.lastUpdateTime) >= 0.25 then
|
||||
TE:FireUpdate()
|
||||
end
|
||||
|
||||
if mod(_floor(now), 10) == 0 and _floor(now) ~= _floor(TE.queryTimer - TE.queryInterval) then
|
||||
TE:PurgeStaleTargets()
|
||||
end
|
||||
end)
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Public helpers for Parser integration
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:IsAPIActiveForTarget(targetKey)
|
||||
if not targetKey then return false end
|
||||
local td = self.targets[targetKey]
|
||||
return td and td.source == "api"
|
||||
end
|
||||
|
||||
function TE:GetCurrentTargetKey()
|
||||
return GetTargetKey("target")
|
||||
end
|
||||
|
||||
TE.GetTargetKey = GetTargetKey
|
||||
TE.IsEliteOrBoss = IsEliteOrBoss
|
||||
TE.IsInGroup = IsInGroup
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Public API: NanamiDPS.ThreatEngine:QueryUnitThreat(unitID)
|
||||
--
|
||||
-- Returns a table with the caller's threat data for the given unit, or nil.
|
||||
-- Intended for external addons (e.g., nameplate addons) to query threat info.
|
||||
--
|
||||
-- @param unitID string WoW unit ID (e.g., "target", "mouseover", etc.)
|
||||
-- @return table or nil { pct, threat, tankName, tankThreat, isTanking,
|
||||
-- isMelee, source, secondName, secondPct }
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:QueryUnitThreat(unitID)
|
||||
if not unitID or not UnitExists(unitID) then return nil end
|
||||
if not self.inCombat then return nil end
|
||||
|
||||
local targetKey = GetTargetKey(unitID)
|
||||
if not targetKey then return nil end
|
||||
|
||||
local td = self.targets[targetKey]
|
||||
if not td then return nil end
|
||||
|
||||
local myData = td.players[self.playerName]
|
||||
local myThreat = myData and myData.threat or 0
|
||||
local myPct = myData and myData.perc or 0
|
||||
|
||||
local secondName, secondThreat, secondPct = nil, 0, 0
|
||||
local sorted = {}
|
||||
for name, pd in _pairs(td.players) do
|
||||
table.insert(sorted, { name = name, threat = pd.threat })
|
||||
end
|
||||
table.sort(sorted, function(a, b) return a.threat > b.threat end)
|
||||
if sorted[2] then
|
||||
secondName = sorted[2].name
|
||||
secondThreat = sorted[2].threat
|
||||
secondPct = td.tankThreat > 0 and (secondThreat / td.tankThreat * 100) or 0
|
||||
end
|
||||
|
||||
return {
|
||||
pct = myPct,
|
||||
threat = myThreat,
|
||||
tankName = td.tankName,
|
||||
tankThreat = td.tankThreat,
|
||||
isTanking = myData and myData.isTanking or false,
|
||||
isMelee = myData and myData.isMelee or false,
|
||||
source = td.source,
|
||||
secondName = secondName,
|
||||
secondPct = secondPct,
|
||||
secondThreat = secondThreat,
|
||||
}
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Public API: NanamiDPS.ThreatEngine:QueryNameThreat(targetKey, playerName)
|
||||
--
|
||||
-- Query a specific player's threat on a specific target.
|
||||
-- @param targetKey string Target key from GetTargetKey()
|
||||
-- @param playerName string Player name
|
||||
-- @return number, boolean, number threat, isTanking, pct
|
||||
-------------------------------------------------------------------------------
|
||||
function TE:QueryNameThreat(targetKey, playerName)
|
||||
if not targetKey or not playerName then return 0, false, 0 end
|
||||
local td = self.targets[targetKey]
|
||||
if not td then return 0, false, 0 end
|
||||
local pd = td.players[playerName]
|
||||
if not pd then return 0, false, 0 end
|
||||
return pd.threat, pd.isTanking, pd.perc
|
||||
end
|
||||
Reference in New Issue
Block a user