339 lines
10 KiB
Lua
339 lines
10 KiB
Lua
local NanamiDPS = NanamiDPS
|
|
local DataStore = NanamiDPS.DataStore
|
|
|
|
local Parser = CreateFrame("Frame")
|
|
NanamiDPS.Parser = Parser
|
|
|
|
local validUnits = { ["player"] = true }
|
|
for i = 1, 4 do validUnits["party" .. i] = true end
|
|
for i = 1, 40 do validUnits["raid" .. i] = true end
|
|
|
|
local validPets = { ["pet"] = true }
|
|
for i = 1, 4 do validPets["partypet" .. i] = true end
|
|
for i = 1, 40 do validPets["raidpet" .. i] = true end
|
|
|
|
Parser.validUnits = validUnits
|
|
Parser.validPets = validPets
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Threat calculation now delegated to ThreatEngine + ThreatCoefficients.
|
|
-- The engine handles stance/buff scanning, talent multipliers, and the
|
|
-- complete spell coefficient database. These wrappers remain for
|
|
-- backward compatibility with ProcessDamage/ProcessHealing.
|
|
-----------------------------------------------------------------------
|
|
|
|
function Parser:CalculateSpellThreat(source, spell, damage)
|
|
if not damage or damage <= 0 then return 0 end
|
|
local class = DataStore:GetClass(source)
|
|
return NanamiDPS.ThreatEngine:CalculateLocalThreat(source, spell, damage, false, class)
|
|
end
|
|
|
|
function Parser:CalculateHealThreat(source, amount)
|
|
if not amount or amount <= 0 then return 0 end
|
|
local class = DataStore:GetClass(source)
|
|
return NanamiDPS.ThreatEngine:CalculateLocalThreat(source, nil, amount, true, class)
|
|
end
|
|
|
|
local unit_cache = {}
|
|
function Parser:UnitByName(name)
|
|
if unit_cache[name] and UnitName(unit_cache[name]) == name then
|
|
return unit_cache[name]
|
|
end
|
|
for unit in pairs(validUnits) do
|
|
if UnitName(unit) == name then
|
|
unit_cache[name] = unit
|
|
return unit
|
|
end
|
|
end
|
|
for unit in pairs(validPets) do
|
|
if UnitName(unit) == name then
|
|
unit_cache[name] = unit
|
|
return unit
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Parser:ScanName(name)
|
|
if not name then return nil end
|
|
|
|
for unit in pairs(validUnits) do
|
|
if UnitExists(unit) and UnitName(unit) == name then
|
|
if UnitIsPlayer(unit) then
|
|
local _, class = UnitClass(unit)
|
|
DataStore:SetClass(name, class)
|
|
return "PLAYER"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SuperWoW pet format: "PetName (OwnerName)"
|
|
local match, _, owner = string.find(name, "%((.*)%)", 1)
|
|
if match and owner then
|
|
if Parser:ScanName(owner) == "PLAYER" then
|
|
DataStore:SetClass(name, owner)
|
|
return "PET"
|
|
end
|
|
end
|
|
|
|
for unit in pairs(validPets) do
|
|
if UnitExists(unit) and UnitName(unit) == name then
|
|
if strsub(unit, 0, 3) == "pet" then
|
|
DataStore:SetClass(name, UnitName("player"))
|
|
elseif strsub(unit, 0, 8) == "partypet" then
|
|
DataStore:SetClass(name, UnitName("party" .. strsub(unit, 9)))
|
|
elseif strsub(unit, 0, 7) == "raidpet" then
|
|
DataStore:SetClass(name, UnitName("raid" .. strsub(unit, 8)))
|
|
end
|
|
return "PET"
|
|
end
|
|
end
|
|
|
|
if NanamiDPS.config and NanamiDPS.config.trackAllUnits then
|
|
DataStore:SetClass(name, DataStore:GetClass(name) or "__other__")
|
|
return "OTHER"
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function Parser:ResolveSource(source)
|
|
source = NanamiDPS.trim(source)
|
|
|
|
local sourceType = self:ScanName(source)
|
|
if not sourceType then return nil, nil end
|
|
|
|
if NanamiDPS.config and NanamiDPS.config.mergePets and sourceType == "PET" then
|
|
local ownerClass = DataStore:GetClass(source)
|
|
if ownerClass and ownerClass ~= "__other__" and NanamiDPS.validClasses[DataStore:GetClass(ownerClass)] then
|
|
return ownerClass, "Pet: " .. source
|
|
elseif ownerClass and ownerClass ~= "__other__" then
|
|
return ownerClass, "Pet: " .. source
|
|
end
|
|
end
|
|
|
|
return source, nil
|
|
end
|
|
|
|
function Parser:ProcessDamage(source, spell, target, amount, school)
|
|
if NanamiDPS.config and NanamiDPS.config.paused then return end
|
|
if type(source) ~= "string" or not tonumber(amount) then return end
|
|
source = NanamiDPS.trim(source)
|
|
amount = tonumber(amount)
|
|
|
|
if source == target then return end
|
|
|
|
local resolvedSource, petPrefix = self:ResolveSource(source)
|
|
if not resolvedSource then return end
|
|
|
|
local finalSpell = petPrefix and (petPrefix .. ": " .. (spell or "")) or spell
|
|
|
|
if not DataStore.inCombat then
|
|
DataStore:StartCombat()
|
|
end
|
|
|
|
DataStore:AddDamage(resolvedSource, finalSpell, target, amount, school)
|
|
|
|
local targetType = self:ScanName(target)
|
|
if targetType == "PLAYER" then
|
|
DataStore:AddDamageTaken(target, finalSpell, resolvedSource, amount, school)
|
|
end
|
|
|
|
local threatAmount = self:CalculateSpellThreat(source, spell, amount)
|
|
DataStore:AddThreat(source, threatAmount)
|
|
|
|
local TE = NanamiDPS.ThreatEngine
|
|
local TC = NanamiDPS.ThreatCoefficients
|
|
if TE then
|
|
local targetKey = TE:GetCurrentTargetKey()
|
|
if targetKey and not TE:IsAPIActiveForTarget(targetKey) then
|
|
TE:AddLocalThreat(targetKey, source, threatAmount)
|
|
|
|
if spell and TC and TC.tauntSpells and TC.tauntSpells[spell] then
|
|
TE:HandleTaunt(source, targetKey)
|
|
end
|
|
end
|
|
end
|
|
|
|
DataStore:UpdateActivity(resolvedSource, GetTime())
|
|
|
|
if targetType == "PLAYER" then
|
|
self:FeedDeathLog(target, {
|
|
time = GetTime(),
|
|
source = resolvedSource,
|
|
spell = finalSpell,
|
|
amount = -amount,
|
|
type = "damage",
|
|
hp = self:GetUnitHP(target),
|
|
})
|
|
end
|
|
|
|
self:ThrottledRefresh()
|
|
end
|
|
|
|
function Parser:ProcessHealing(source, spell, target, amount, school)
|
|
if NanamiDPS.config and NanamiDPS.config.paused then return end
|
|
if type(source) ~= "string" or not tonumber(amount) then return end
|
|
source = NanamiDPS.trim(source)
|
|
amount = tonumber(amount)
|
|
|
|
local resolvedSource, petPrefix = self:ResolveSource(source)
|
|
if not resolvedSource then return end
|
|
|
|
local finalSpell = petPrefix and (petPrefix .. ": " .. (spell or "")) or spell
|
|
|
|
local effective = amount
|
|
local unitstr = self:UnitByName(target)
|
|
if unitstr then
|
|
effective = math.min(UnitHealthMax(unitstr) - UnitHealth(unitstr), amount)
|
|
end
|
|
|
|
if not DataStore.inCombat then
|
|
DataStore:StartCombat()
|
|
end
|
|
|
|
DataStore:AddHealing(resolvedSource, finalSpell, target, amount, effective)
|
|
|
|
local healThreat = self:CalculateHealThreat(source, effective)
|
|
DataStore:AddThreat(source, healThreat)
|
|
|
|
local TE = NanamiDPS.ThreatEngine
|
|
if TE then
|
|
local targetKey = TE:GetCurrentTargetKey()
|
|
if targetKey and not TE:IsAPIActiveForTarget(targetKey) then
|
|
TE:AddLocalThreat(targetKey, source, healThreat)
|
|
end
|
|
end
|
|
|
|
DataStore:UpdateActivity(resolvedSource, GetTime())
|
|
|
|
local targetType = self:ScanName(target)
|
|
if targetType == "PLAYER" then
|
|
self:FeedDeathLog(target, {
|
|
time = GetTime(),
|
|
source = resolvedSource,
|
|
spell = finalSpell,
|
|
amount = effective,
|
|
type = "heal",
|
|
hp = self:GetUnitHP(target),
|
|
})
|
|
end
|
|
|
|
self:ThrottledRefresh()
|
|
end
|
|
|
|
function Parser:ProcessDeath(name)
|
|
if not name then return end
|
|
local nameType = self:ScanName(name)
|
|
if nameType ~= "PLAYER" then return end
|
|
|
|
local log = self:FlushDeathLog(name)
|
|
DataStore:AddDeath(name, {
|
|
time = GetTime(),
|
|
timeStr = date("%H:%M:%S"),
|
|
events = log,
|
|
})
|
|
|
|
self:ThrottledRefresh()
|
|
end
|
|
|
|
function Parser:ProcessDispel(source, spell, target, aura)
|
|
if type(source) ~= "string" then return end
|
|
source = NanamiDPS.trim(source)
|
|
|
|
local resolvedSource = self:ResolveSource(source)
|
|
if not resolvedSource then return end
|
|
|
|
DataStore:AddDispel(resolvedSource, spell or NanamiDPS.L["Unknown"], target, aura)
|
|
DataStore:UpdateActivity(resolvedSource, GetTime())
|
|
|
|
self:ThrottledRefresh()
|
|
end
|
|
|
|
function Parser:ProcessInterrupt(source, spell, target, interrupted)
|
|
if type(source) ~= "string" then return end
|
|
source = NanamiDPS.trim(source)
|
|
|
|
local resolvedSource = self:ResolveSource(source)
|
|
if not resolvedSource then return end
|
|
|
|
DataStore:AddInterrupt(resolvedSource, spell or NanamiDPS.L["Unknown"], target, interrupted)
|
|
DataStore:UpdateActivity(resolvedSource, GetTime())
|
|
|
|
self:ThrottledRefresh()
|
|
end
|
|
|
|
function Parser:GetUnitHP(name)
|
|
local unit = self:UnitByName(name)
|
|
if unit then
|
|
return UnitHealth(unit), UnitHealthMax(unit)
|
|
end
|
|
return 0, 0
|
|
end
|
|
|
|
-- Throttle refresh callbacks to avoid excessive UI updates
|
|
Parser.lastRefreshTime = 0
|
|
local REFRESH_INTERVAL = 0.25
|
|
|
|
function Parser:ThrottledRefresh()
|
|
local now = GetTime()
|
|
if now - self.lastRefreshTime >= REFRESH_INTERVAL then
|
|
self.lastRefreshTime = now
|
|
NanamiDPS:FireCallback("refresh")
|
|
end
|
|
end
|
|
|
|
-- Death log buffer: last 15 seconds of events per player
|
|
Parser.deathLogBuffer = {}
|
|
local DEATH_LOG_WINDOW = 15
|
|
|
|
function Parser:FeedDeathLog(name, entry)
|
|
if not self.deathLogBuffer[name] then
|
|
self.deathLogBuffer[name] = {}
|
|
end
|
|
table.insert(self.deathLogBuffer[name], entry)
|
|
|
|
-- Trim old entries
|
|
local now = GetTime()
|
|
local buf = self.deathLogBuffer[name]
|
|
while table.getn(buf) > 0 and (now - buf[1].time) > DEATH_LOG_WINDOW do
|
|
table.remove(buf, 1)
|
|
end
|
|
end
|
|
|
|
function Parser:FlushDeathLog(name)
|
|
local log = self.deathLogBuffer[name] or {}
|
|
self.deathLogBuffer[name] = {}
|
|
return log
|
|
end
|
|
|
|
-- Combat state detection
|
|
local combatFrame = CreateFrame("Frame", "NanamiDPSCombatState", UIParent)
|
|
combatFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
combatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
|
|
local pendingCombatEnd = false
|
|
local combatEndTimer = 0
|
|
|
|
combatFrame:SetScript("OnEvent", function()
|
|
if event == "PLAYER_REGEN_DISABLED" then
|
|
pendingCombatEnd = false
|
|
if not DataStore.inCombat then
|
|
DataStore:StartCombat()
|
|
end
|
|
elseif event == "PLAYER_REGEN_ENABLED" then
|
|
pendingCombatEnd = true
|
|
combatEndTimer = GetTime() + 2
|
|
end
|
|
end)
|
|
|
|
combatFrame:SetScript("OnUpdate", function()
|
|
if (this.tick or 1) > GetTime() then return end
|
|
this.tick = GetTime() + 1
|
|
|
|
if pendingCombatEnd and GetTime() >= combatEndTimer then
|
|
pendingCombatEnd = false
|
|
DataStore:StopCombat()
|
|
end
|
|
end)
|