Files
Nanami-DPS/Parser.lua
2026-03-25 00:57:35 +08:00

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)